/*************************************************************************
jquery.dynatree.js
Dynamic tree view control, with support for lazy loading of branches.

Copyright (c) 2008-2011, Martin Wendt (http://wwWendt.de)
Dual licensed under the MIT or GPL Version 2 licenses.
http://code.google.com/p/dynatree/wiki/LicenseInfo

A current version and some documentation is available at
http://dynatree.googlecode.com/

$Version: 1.2.0$
$Revision: 528, 2011-09-17 18:58:59$

@depends: jquery.js
@depends: jquery.ui.core.js
@depends: jquery.cookie.js
*************************************************************************/

// Note: We currently allow eval() to parse the 'data' attribtes, when initializing from HTML.
/*jslint laxbreak: true, browser: true, evil: true, indent: 0, white: false, onevar: false */

/*************************************************************************
*	Debug functions
*/

var _canLog = true;

function _log(mode, msg) {
    /**
    * Usage: logMsg("%o was toggled", this);
    */
    if (!_canLog) {
        return;
    }
    // Remove first argument
    var args = Array.prototype.slice.apply(arguments, [1]);
    // Prepend timestamp
    var dt = new Date();
    var tag = dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds() + "." + dt.getMilliseconds();
    args[0] = tag + " - " + args[0];

    try {
        switch (mode) {
            case "info":
                window.console.info.apply(window.console, args);
                break;
            case "warn":
                window.console.warn.apply(window.console, args);
                break;
            default:
                window.console.log.apply(window.console, args);
                break;
        }
    } catch (e) {
        if (!window.console) {
            _canLog = false; // Permanently disable, when logging is not supported by the browser
        }
    }
}

function logMsg(msg) {
    Array.prototype.unshift.apply(arguments, ["debug"]);
    _log.apply(this, arguments);
}


// Forward declaration
var getDynaTreePersistData = null;



/*************************************************************************
*	Constants
*/
var DTNodeStatus_Error = -1;
var DTNodeStatus_Loading = 1;
var DTNodeStatus_Ok = 0;


// Start of local namespace
(function ($) {

    /*************************************************************************
    *	Common tool functions.
    */

    var Class = {
        create: function () {
            return function () {
                this.initialize.apply(this, arguments);
            };
        }
    };

    // Tool function to get dtnode from the event target:
    function getDtNodeFromElement(el) {
        var iMax = 5;
        while (el && iMax--) {
            if (el.dtnode) { return el.dtnode; }
            el = el.parentNode;
        }
        return null;
    }

    function noop() {
    }

    /*************************************************************************
    *	Class DynaTreeNode
    */
    var DynaTreeNode = Class.create();

    DynaTreeNode.prototype = {
        initialize: function (parent, tree, data) {
            /**
            * @constructor
            */
            this.parent = parent;
            this.tree = tree;
            if (typeof data === "string") {
                data = { title: data };
            }
            if (data.key === undefined) {
                data.key = "_" + tree._nodeCount++;
            }
            this.data = $.extend({}, $.ui.dynatree.nodedatadefaults, data);
            this.li = null; // not yet created
            this.span = null; // not yet created
            this.ul = null; // not yet created
            this.childList = null; // no subnodes yet
            this.isLoading = false; // Lazy content is being loaded
            this.hasSubSel = false;
            this.bExpanded = false;
            this.bSelected = false;

        },

        toString: function () {
            return "DynaTreeNode<" + this.data.key + ">: '" + this.data.title + "'";
        },

        toDict: function (recursive, callback) {
            var dict = $.extend({}, this.data);
            dict.activate = (this.tree.activeNode === this);
            dict.focus = (this.tree.focusNode === this);
            dict.expand = this.bExpanded;
            dict.select = this.bSelected;
            if (callback) {
                callback(dict);
            }
            if (recursive && this.childList) {
                dict.children = [];
                for (var i = 0, l = this.childList.length; i < l; i++) {
                    dict.children.push(this.childList[i].toDict(true, callback));
                }
            } else {
                delete dict.children;
            }
            return dict;
        },

        fromDict: function (dict) {
            /**
            * Update node data. If dict contains 'children', then also replace
            * the hole sub tree.
            */
            var children = dict.children;
            if (children === undefined) {
                this.data = $.extend(this.data, dict);
                this.render();
                return;
            }
            dict = $.extend({}, dict);
            dict.children = undefined;
            this.data = $.extend(this.data, dict);
            this.removeChildren();
            this.addChild(children);
        },

        _getInnerHtml: function () {
            var tree = this.tree,
			opts = tree.options,
			cache = tree.cache,
			level = this.getLevel(),
			data = this.data,
			res = "";
            // connector (expanded, expandable or simple)
            if (level < opts.minExpandLevel) {
                if (level > 1) {
                    res += cache.tagConnector;
                }
                // .. else (i.e. for root level) skip expander/connector altogether
            } else if (this.hasChildren() !== false) {
                res += cache.tagExpander;
            } else {
                res += cache.tagConnector;
            }
            // Checkbox mode
            if (opts.checkbox && data.hideCheckbox !== true && !data.isStatusNode) {
                res += cache.tagCheckbox;
            }
            // folder or doctype icon
            if (data.icon) {
                res += "<img src='" + opts.imagePath + data.icon + "' alt='' />";
            } else if (data.icon === false) {
                // icon == false means 'no icon'
                noop(); // keep JSLint happy
            } else {
                // icon == null means 'default icon'
                res += cache.tagNodeIcon;
            }
            // node title
            var nodeTitle = "";
            if (opts.onCustomRender) {
                nodeTitle = opts.onCustomRender.call(tree, this) || "";
            }
            if (!nodeTitle) {
                var tooltip = data.tooltip ? " title='" + data.tooltip + "'" : "";
                if (opts.noLink || data.noLink) {
                    nodeTitle = "<span style='display: inline-block;' class='" + opts.classNames.title + "'" + tooltip + ">" + data.title + "</span>";
                    //				this.tree.logDebug("nodeTitle: " + nodeTitle);
                } else {
                    nodeTitle = "<a href='#' class='" + opts.classNames.title + "'" + tooltip + ">" + data.title + "</a>";
                }
            }
            res += nodeTitle;
            return res;
        },


        _fixOrder: function () {
            /**
            * Make sure, that <li> order matches childList order.
            */
            var cl = this.childList;
            if (!cl || !this.ul) {
                return;
            }
            var childLI = this.ul.firstChild;
            for (var i = 0, l = cl.length - 1; i < l; i++) {
                var childNode1 = cl[i];
                var childNode2 = childLI.dtnode;
                if (childNode1 !== childNode2) {
                    this.tree.logDebug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2);
                    this.ul.insertBefore(childNode1.li, childNode2.li);
                } else {
                    childLI = childLI.nextSibling;
                }
            }
        },


        render: function (useEffects, includeInvisible) {
            /**
            * Create <li><span>..</span> .. </li> tags for this node.
            *
            * <li id='KEY' dtnode=NODE> // This div contains the node's span and list of child div's.
            *   <span class='title'>S S S A</span> // Span contains graphic spans and title <a> tag
            *   <ul> // only present, when node has children
            *       <li id='KEY' dtnode=NODE>child1</li>
            *       <li id='KEY' dtnode=NODE>child2</li>
            *   </ul>
            * </li>
            */
            //		this.tree.logDebug("%s.render(%s)", this, useEffects);
            // ---
            var tree = this.tree,
			parent = this.parent,
			data = this.data,
			opts = tree.options,
			cn = opts.classNames,
			isLastSib = this.isLastSibling(),
			firstTime = false;

            if (!parent && !this.ul) {
                // Root node has only a <ul>
                this.li = this.span = null;
                this.ul = document.createElement("ul");
                if (opts.minExpandLevel > 1) {
                    this.ul.className = cn.container + " " + cn.noConnector;
                } else {
                    this.ul.className = cn.container;
                }
            } else if (parent) {
                // Create <li><span /> </li>
                if (!this.li) {
                    firstTime = true;
                    this.li = document.createElement("li");
                    this.li.dtnode = this;
                    if (data.key && opts.generateIds) {
                        this.li.id = opts.idPrefix + data.key;
                    }
                    this.span = document.createElement("span");
                    this.span.className = cn.title;
                    this.li.appendChild(this.span);

                    if (!parent.ul) {
                        // This is the parent's first child: create UL tag
                        // (Hidden, because it will be
                        parent.ul = document.createElement("ul");
                        parent.ul.style.display = "none";
                        parent.li.appendChild(parent.ul);
                        //					if( opts.minExpandLevel > this.getLevel() ){
                        //						parent.ul.className = cn.noConnector;
                        //					}
                    }
                    // set node connector images, links and text
                    //				this.span.innerHTML = this._getInnerHtml();

                    parent.ul.appendChild(this.li);
                }
                // set node connector images, links and text
                this.span.innerHTML = this._getInnerHtml();
                // Set classes for current status
                var cnList = [];
                cnList.push(cn.node);
                if (data.isFolder) {
                    cnList.push(cn.folder);
                }
                if (this.bExpanded) {
                    cnList.push(cn.expanded);
                }
                if (this.hasChildren() !== false) {
                    cnList.push(cn.hasChildren);
                }
                if (data.isLazy && this.childList === null) {
                    cnList.push(cn.lazy);
                }
                if (isLastSib) {
                    cnList.push(cn.lastsib);
                }
                if (this.bSelected) {
                    cnList.push(cn.selected);
                }
                if (this.hasSubSel) {
                    cnList.push(cn.partsel);
                }
                if (tree.activeNode === this) {
                    cnList.push(cn.active);
                }
                if (data.addClass) {
                    cnList.push(data.addClass);
                }
                // IE6 doesn't correctly evaluate multiple class names,
                // so we create combined class names that can be used in the CSS
                cnList.push(cn.combinedExpanderPrefix
					+ (this.bExpanded ? "e" : "c")
					+ (data.isLazy && this.childList === null ? "d" : "")
					+ (isLastSib ? "l" : "")
					);
                cnList.push(cn.combinedIconPrefix
					+ (this.bExpanded ? "e" : "c")
					+ (data.isFolder ? "f" : "")
					);
                this.span.className = cnList.join(" ");

                // TODO: we should not set this in the <span> tag also, if we set it here:
                this.li.className = isLastSib ? cn.lastsib : "";

                // Allow tweaking, binding, after node was created for the first time
                if (firstTime && opts.onCreate) {
                    opts.onCreate.call(tree, this, this.span);
                }
                // Hide children, if node is collapsed
                //			this.ul.style.display = ( this.bExpanded || !parent ) ? "" : "none";
                // Allow tweaking after node state was rendered
                if (opts.onRender) {
                    opts.onRender.call(tree, this, this.span);
                }
            }
            // Visit child nodes
            if ((this.bExpanded || includeInvisible === true) && this.childList) {
                for (var i = 0, l = this.childList.length; i < l; i++) {
                    this.childList[i].render(false, includeInvisible);
                }
                // Make sure the tag order matches the child array
                this._fixOrder();
            }
            // Hide children, if node is collapsed
            if (this.ul) {
                var isHidden = (this.ul.style.display === "none");
                var isExpanded = !!this.bExpanded;
                //			logMsg("isHidden:%s", isHidden);
                if (useEffects && opts.fx && (isHidden === isExpanded)) {
                    var duration = opts.fx.duration || 200;
                    $(this.ul).animate(opts.fx, duration);
                } else {
                    this.ul.style.display = (this.bExpanded || !parent) ? "" : "none";
                }
            }
        },
        /** Return '/id1/id2/id3'. */
        getKeyPath: function (excludeSelf) {
            var path = [];
            this.visitParents(function (node) {
                if (node.parent) {
                    path.unshift(node.data.key);
                }
            }, !excludeSelf);
            return "/" + path.join(this.tree.options.keyPathSeparator);
        },

        getParent: function () {
            return this.parent;
        },

        getChildren: function () {
            if (this.hasChildren() === undefined) {
                return undefined; // Lazy node: unloaded, currently loading, or load error
            }
            return this.childList;
        },

        /** Check if node has children (returns undefined, if not sure). */
        hasChildren: function () {
            if (this.data.isLazy) {
                if (this.childList === null || this.childList === undefined) {
                    // Not yet loaded
                    return undefined;
                } else if (this.childList.length === 0) {
                    // Loaded, but response was empty
                    return false;
                } else if (this.childList.length === 1 && this.childList[0].isStatusNode()) {
                    // Currently loading or load error
                    return undefined;
                }
                return true;
            }
            return !!this.childList;
        },

        isFirstSibling: function () {
            var p = this.parent;
            return !p || p.childList[0] === this;
        },

        isLastSibling: function () {
            var p = this.parent;
            return !p || p.childList[p.childList.length - 1] === this;
        },

        getPrevSibling: function () {
            if (!this.parent) {
                return null;
            }
            var ac = this.parent.childList;
            for (var i = 1, l = ac.length; i < l; i++) { // start with 1, so prev(first) = null
                if (ac[i] === this) {
                    return ac[i - 1];
                }
            }
            return null;
        },

        getNextSibling: function () {
            if (!this.parent) {
                return null;
            }
            var ac = this.parent.childList;
            for (var i = 0, l = ac.length - 1; i < l; i++) { // up to length-2, so next(last) = null
                if (ac[i] === this) {
                    return ac[i + 1];
                }
            }
            return null;
        },

        isStatusNode: function () {
            return (this.data.isStatusNode === true);
        },

        isChildOf: function (otherNode) {
            return (this.parent && this.parent === otherNode);
        },

        isDescendantOf: function (otherNode) {
            if (!otherNode) {
                return false;
            }
            var p = this.parent;
            while (p) {
                if (p === otherNode) {
                    return true;
                }
                p = p.parent;
            }
            return false;
        },

        countChildren: function () {
            var cl = this.childList;
            if (!cl) {
                return 0;
            }
            var n = cl.length;
            for (var i = 0, l = n; i < l; i++) {
                var child = cl[i];
                n += child.countChildren();
            }
            return n;
        },

        /**Sort child list by title.
        * cmd: optional compare function.
        * deep: optional: pass true to sort all descendant nodes.
        */
        sortChildren: function (cmp, deep) {
            var cl = this.childList;
            if (!cl) {
                return;
            }
            cmp = cmp || function (a, b) {
                //			return a.data.title === b.data.title ? 0 : a.data.title > b.data.title ? 1 : -1;
                var x = a.data.title.toLowerCase(),
				y = b.data.title.toLowerCase();
                return x === y ? 0 : x > y ? 1 : -1;
            };
            cl.sort(cmp);
            if (deep) {
                for (var i = 0, l = cl.length; i < l; i++) {
                    if (cl[i].childList) {
                        cl[i].sortChildren(cmp, "$norender$");
                    }
                }
            }
            if (deep !== "$norender$") {
                this.render();
            }
        },

        _setStatusNode: function (data) {
            // Create, modify or remove the status child node (pass 'null', to remove it).
            var firstChild = (this.childList ? this.childList[0] : null);
            if (!data) {
                if (firstChild && firstChild.isStatusNode()) {
                    try {
                        // I've seen exceptions here with loadKeyPath...
                        if (this.ul) {
                            this.ul.removeChild(firstChild.li);
                            firstChild.li = null; // avoid leaks (issue 215)
                        }
                    } catch (e) { }
                    if (this.childList.length === 1) {
                        this.childList = [];
                    } else {
                        this.childList.shift();
                    }
                }
            } else if (firstChild) {
                data.isStatusNode = true;
                data.key = "_statusNode";
                firstChild.data = data;
                firstChild.render();
            } else {
                data.isStatusNode = true;
                data.key = "_statusNode";
                firstChild = this.addChild(data);
            }
        },

        setLazyNodeStatus: function (lts, opts) {
            var tooltip = (opts && opts.tooltip) ? opts.tooltip : null;
            var info = (opts && opts.info) ? " (" + opts.info + ")" : "";
            switch (lts) {
                case DTNodeStatus_Ok:
                    this._setStatusNode(null);
                    $(this.span).removeClass(this.tree.options.classNames.nodeLoading);
                    this.isLoading = false;
                    //				this.render();
                    if (this.tree.options.autoFocus) {
                        if (this === this.tree.tnRoot && this.childList && this.childList.length > 0) {
                            // special case: using ajaxInit
                            this.childList[0].focus();
                        } else {
                            this.focus();
                        }
                    }
                    break;
                case DTNodeStatus_Loading:
                    this.isLoading = true;
                    $(this.span).addClass(this.tree.options.classNames.nodeLoading);
                    // The root is hidden, so we set a temporary status child
                    if (!this.parent) {
                        this._setStatusNode({
                            title: this.tree.options.strings.loading + info,
                            tooltip: tooltip,
                            addClass: this.tree.options.classNames.nodeWait
                        });
                    }
                    break;
                case DTNodeStatus_Error:
                    this.isLoading = false;
                    //				$(this.span).addClass(this.tree.options.classNames.nodeError);
                    this._setStatusNode({
                        title: this.tree.options.strings.loadError + info,
                        tooltip: tooltip,
                        addClass: this.tree.options.classNames.nodeError
                    });
                    break;
                default:
                    throw "Bad LazyNodeStatus: '" + lts + "'.";
            }
        },

        _parentList: function (includeRoot, includeSelf) {
            var l = [];
            var dtn = includeSelf ? this : this.parent;
            while (dtn) {
                if (includeRoot || dtn.parent) {
                    l.unshift(dtn);
                }
                dtn = dtn.parent;
            }
            return l;
        },
        getLevel: function () {
            /**
            * Return node depth. 0: System root node, 1: visible top-level node.
            */
            var level = 0;
            var dtn = this.parent;
            while (dtn) {
                level++;
                dtn = dtn.parent;
            }
            return level;
        },

        _getTypeForOuterNodeEvent: function (event) {
            /** Return the inner node span (title, checkbox or expander) if
            *  event.target points to the outer span.
            *  This function should fix issue #93:
            *  FF2 ignores empty spans, when generating events (returning the parent instead).
            */
            var cns = this.tree.options.classNames;
            var target = event.target;
            // Only process clicks on an outer node span (probably due to a FF2 event handling bug)
            if (target.className.indexOf(cns.node) < 0) {
                return null;
            }
            // Event coordinates, relative to outer node span:
            var eventX = event.pageX - target.offsetLeft;
            var eventY = event.pageY - target.offsetTop;

            for (var i = 0, l = target.childNodes.length; i < l; i++) {
                var cn = target.childNodes[i];
                var x = cn.offsetLeft - target.offsetLeft;
                var y = cn.offsetTop - target.offsetTop;
                var nx = cn.clientWidth, ny = cn.clientHeight;
                //	        alert (cn.className + ": " + x + ", " + y + ", s:" + nx + ", " + ny);
                if (eventX >= x && eventX <= (x + nx) && eventY >= y && eventY <= (y + ny)) {
                    //	            alert("HIT "+ cn.className);
                    if (cn.className == cns.title) {
                        return "title";
                    } else if (cn.className == cns.expander) {
                        return "expander";
                    } else if (cn.className == cns.checkbox) {
                        return "checkbox";
                    } else if (cn.className == cns.nodeIcon) {
                        return "icon";
                    }
                }
            }
            return "prefix";
        },

        getEventTargetType: function (event) {
            // Return the part of a node, that a click event occured on.
            // Note: there is no check, if the event was fired on TIHS node.
            var tcn = event && event.target ? event.target.className : "";
            var cns = this.tree.options.classNames;

            if (tcn === cns.title) {
                return "title";
            } else if (tcn === cns.expander) {
                return "expander";
            } else if (tcn === cns.checkbox) {
                return "checkbox";
            } else if (tcn === cns.nodeIcon) {
                return "icon";
            } else if (tcn === cns.empty || tcn === cns.vline || tcn === cns.connector) {
                return "prefix";
            } else if (tcn.indexOf(cns.node) >= 0) {
                // FIX issue #93
                return this._getTypeForOuterNodeEvent(event);
            }
            return null;
        },

        isVisible: function () {
            // Return true, if all parents are expanded.
            var parents = this._parentList(true, false);
            for (var i = 0, l = parents.length; i < l; i++) {
                if (!parents[i].bExpanded) { return false; }
            }
            return true;
        },

        makeVisible: function () {
            // Make sure, all parents are expanded
            var parents = this._parentList(true, false);
            for (var i = 0, l = parents.length; i < l; i++) {
                parents[i]._expand(true);
            }
        },

        focus: function () {
            // TODO: check, if we already have focus
            //		this.tree.logDebug("dtnode.focus(): %o", this);
            this.makeVisible();
            try {
                $(this.span).find(">a").focus();
            } catch (e) { }
        },

        isFocused: function () {
            return (this.tree.tnFocused === this);
        },

        _activate: function (flag, fireEvents) {
            // (De)Activate - but not focus - this node.
            this.tree.logDebug("dtnode._activate(%o, fireEvents=%o) - %o", flag, fireEvents, this);
            var opts = this.tree.options;
            if (this.data.isStatusNode) {
                return;
            }
            if (fireEvents && opts.onQueryActivate && opts.onQueryActivate.call(this.tree, flag, this) === false) {
                return; // Callback returned false
            }
            if (flag) {
                // Activate
                if (this.tree.activeNode) {
                    if (this.tree.activeNode === this) {
                        return;
                    }
                    this.tree.activeNode.deactivate();
                }
                if (opts.activeVisible) {
                    this.makeVisible();
                }
                this.tree.activeNode = this;
                if (opts.persist) {
                    $.cookie(opts.cookieId + "-active", this.data.key, opts.cookie);
                }
                this.tree.persistence.activeKey = this.data.key;
                $(this.span).addClass(opts.classNames.active);
                if (fireEvents && opts.onActivate) {
                    opts.onActivate.call(this.tree, this);
                }
            } else {
                // Deactivate
                if (this.tree.activeNode === this) {
                    if (opts.onQueryActivate && opts.onQueryActivate.call(this.tree, false, this) === false) {
                        return; // Callback returned false
                    }
                    $(this.span).removeClass(opts.classNames.active);
                    if (opts.persist) {
                        // Note: we don't pass null, but ''. So the cookie is not deleted.
                        // If we pass null, we also have to pass a COPY of opts, because $cookie will override opts.expires (issue 84)
                        $.cookie(opts.cookieId + "-active", "", opts.cookie);
                    }
                    this.tree.persistence.activeKey = null;
                    this.tree.activeNode = null;
                    if (fireEvents && opts.onDeactivate) {
                        opts.onDeactivate.call(this.tree, this);
                    }
                }
            }
        },

        activate: function () {
            // Select - but not focus - this node.
            //		this.tree.logDebug("dtnode.activate(): %o", this);
            this._activate(true, true);
        },

        activateSilently: function () {
            this._activate(true, false);
        },

        deactivate: function () {
            //		this.tree.logDebug("dtnode.deactivate(): %o", this);
            this._activate(false, true);
        },

        isActive: function () {
            return (this.tree.activeNode === this);
        },

        _userActivate: function () {
            // Handle user click / [space] / [enter], according to clickFolderMode.
            var activate = true;
            var expand = false;
            if (this.data.isFolder) {
                switch (this.tree.options.clickFolderMode) {
                    case 2:
                        activate = false;
                        expand = true;
                        break;
                    case 3:
                        activate = expand = true;
                        break;
                }
            }
            if (this.parent === null) {
                expand = false;
            }
            if (expand) {
                this.toggleExpand();
                this.focus();
            }
            if (activate) {
                this.activate();
            }
        },

        _setSubSel: function (hasSubSel) {
            if (hasSubSel) {
                this.hasSubSel = true;
                $(this.span).addClass(this.tree.options.classNames.partsel);
            } else {
                this.hasSubSel = false;
                $(this.span).removeClass(this.tree.options.classNames.partsel);
            }
        },
        /**
        * Fix selection and partsel status, of parent nodes, according to current status of
        * end nodes.
        */
        _updatePartSelectionState: function () {
            //		alert("_updatePartSelectionState " + this);
            //		this.tree.logDebug("_updatePartSelectionState() - %o", this);
            var sel;
            // Return `true` or `false` for end nodes and remove part-sel flag
            if (!this.hasChildren()) {
                sel = (this.bSelected && !this.data.unselectable && !this.data.isStatusNode);
                this._setSubSel(false);
                return sel;
            }
            // Return `true`, `false`, or `undefined` for parent nodes
            var i, l,
			cl = this.childList,
			allSelected = true,
			allDeselected = true;
            for (i = 0, l = cl.length; i < l; i++) {
                var n = cl[i],
				s = n._updatePartSelectionState();
                if (s !== false) {
                    allDeselected = false;
                }
                if (s !== true) {
                    allSelected = false;
                }
            }
            if (allSelected) {
                sel = true;
            } else if (allDeselected) {
                sel = false;
            } else {
                sel = undefined;
            }
            this._setSubSel(sel === undefined);
            this.bSelected = (sel === true);
            return sel;
        },

        /**
        * Fix selection status, after this node was (de)selected in multi-hier mode.
        * This includes (de)selecting all children.
        */
        _fixSelectionState: function () {
            //		alert("_fixSelectionState " + this);
            //		this.tree.logDebug("_fixSelectionState(%s) - %o", this.bSelected, this);
            var p, i, l;
            if (this.bSelected) {
                // Select all children
                this.visit(function (node) {
                    node.parent._setSubSel(true);
                    if (!node.data.unselectable) {
                        node._select(true, false, false);
                    }
                });
                // Select parents, if all children are selected
                p = this.parent;
                while (p) {
                    p._setSubSel(true);
                    var allChildsSelected = true;
                    for (i = 0, l = p.childList.length; i < l; i++) {
                        var n = p.childList[i];
                        if (!n.bSelected && !n.data.isStatusNode && !n.data.unselectable) {
                            allChildsSelected = false;
                            break;
                        }
                    }
                    if (allChildsSelected) {
                        p._select(true, false, false);
                    }
                    p = p.parent;
                }
            } else {
                // Deselect all children
                this._setSubSel(false);
                this.visit(function (node) {
                    node._setSubSel(false);
                    node._select(false, false, false);
                });
                // Deselect parents, and recalc hasSubSel
                p = this.parent;
                while (p) {
                    p._select(false, false, false);
                    var isPartSel = false;
                    for (i = 0, l = p.childList.length; i < l; i++) {
                        if (p.childList[i].bSelected || p.childList[i].hasSubSel) {
                            isPartSel = true;
                            break;
                        }
                    }
                    p._setSubSel(isPartSel);
                    p = p.parent;
                }
            }
        },

        _select: function (sel, fireEvents, deep) {
            // Select - but not focus - this node.
            //		this.tree.logDebug("dtnode._select(%o) - %o", sel, this);
            var opts = this.tree.options;
            if (this.data.isStatusNode) {
                return;
            }
            //
            if (this.bSelected === sel) {
                //			this.tree.logDebug("dtnode._select(%o) IGNORED - %o", sel, this);
                return;
            }
            // Allow event listener to abort selection
            if (fireEvents && opts.onQuerySelect && opts.onQuerySelect.call(this.tree, sel, this) === false) {
                return; // Callback returned false
            }
            // Force single-selection
            if (opts.selectMode == 1 && sel) {
                this.tree.visit(function (node) {
                    if (node.bSelected) {
                        // Deselect; assuming that in selectMode:1 there's max. one other selected node
                        node._select(false, false, false);
                        return false;
                    }
                });
            }

            this.bSelected = sel;
            //        this.tree._changeNodeList("select", this, sel);

            if (sel) {
                if (opts.persist) {
                    this.tree.persistence.addSelect(this.data.key);
                }
                $(this.span).addClass(opts.classNames.selected);

                if (deep && opts.selectMode === 3) {
                    this._fixSelectionState();
                }
                if (fireEvents && opts.onSelect) {
                    opts.onSelect.call(this.tree, true, this);
                }
            } else {
                if (opts.persist) {
                    this.tree.persistence.clearSelect(this.data.key);
                }
                $(this.span).removeClass(opts.classNames.selected);

                if (deep && opts.selectMode === 3) {
                    this._fixSelectionState();
                }
                if (fireEvents && opts.onSelect) {
                    opts.onSelect.call(this.tree, false, this);
                }
            }
        },

        select: function (sel) {
            // Select - but not focus - this node.
            //		this.tree.logDebug("dtnode.select(%o) - %o", sel, this);
            if (this.data.unselectable) {
                return this.bSelected;
            }
            return this._select(sel !== false, true, true);
        },

        toggleSelect: function () {
            //		this.tree.logDebug("dtnode.toggleSelect() - %o", this);
            return this.select(!this.bSelected);
        },

        isSelected: function () {
            return this.bSelected;
        },

        isLazy: function () {
            return !!this.data.isLazy;
        },

        _loadContent: function () {
            try {
                var opts = this.tree.options;
                this.tree.logDebug("_loadContent: start - %o", this);
                this.setLazyNodeStatus(DTNodeStatus_Loading);
                if (true === opts.onLazyRead.call(this.tree, this)) {
                    // If function returns 'true', we assume that the loading is done:
                    this.setLazyNodeStatus(DTNodeStatus_Ok);
                    // Otherwise (i.e. if the loading was started as an asynchronous process)
                    // the onLazyRead(dtnode) handler is expected to call dtnode.setLazyNodeStatus(DTNodeStatus_Ok/_Error) when done.
                    this.tree.logDebug("_loadContent: succeeded - %o", this);
                }
            } catch (e) {
                this.tree.logWarning("_loadContent: failed - %o", e);
                this.setLazyNodeStatus(DTNodeStatus_Error, { tooltip: "" + e });
            }
        },

        _expand: function (bExpand, forceSync) {
            if (this.bExpanded === bExpand) {
                this.tree.logDebug("dtnode._expand(%o) IGNORED - %o", bExpand, this);
                return;
            }
            this.tree.logDebug("dtnode._expand(%o) - %o", bExpand, this);
            var opts = this.tree.options;
            if (!bExpand && this.getLevel() < opts.minExpandLevel) {
                this.tree.logDebug("dtnode._expand(%o) prevented collapse - %o", bExpand, this);
                return;
            }
            if (opts.onQueryExpand && opts.onQueryExpand.call(this.tree, bExpand, this) === false) {
                return; // Callback returned false
            }
            this.bExpanded = bExpand;

            // Persist expand state
            if (opts.persist) {
                if (bExpand) {
                    this.tree.persistence.addExpand(this.data.key);
                } else {
                    this.tree.persistence.clearExpand(this.data.key);
                }
            }
            // Do not apply animations in init phase, or before lazy-loading
            var allowEffects = !(this.data.isLazy && this.childList === null)
			&& !this.isLoading
			&& !forceSync;
            this.render(allowEffects);

            // Auto-collapse mode: collapse all siblings
            if (this.bExpanded && this.parent && opts.autoCollapse) {
                var parents = this._parentList(false, true);
                for (var i = 0, l = parents.length; i < l; i++) {
                    parents[i].collapseSiblings();
                }
            }
            // If the currently active node is now hidden, deactivate it
            if (opts.activeVisible && this.tree.activeNode && !this.tree.activeNode.isVisible()) {
                this.tree.activeNode.deactivate();
            }
            // Expanding a lazy node: set 'loading...' and call callback
            if (bExpand && this.data.isLazy && this.childList === null && !this.isLoading) {
                this._loadContent();
                return;
            }
            if (opts.onExpand) {
                opts.onExpand.call(this.tree, bExpand, this);
            }
        },

        isExpanded: function () {
            return this.bExpanded;
        },

        expand: function (flag) {
            flag = (flag !== false);
            if (!this.childList && !this.data.isLazy && flag) {
                return; // Prevent expanding empty nodes
            } else if (this.parent === null && !flag) {
                return; // Prevent collapsing the root
            }
            this._expand(flag);
        },

        scheduleAction: function (mode, ms) {
            /** Schedule activity for delayed execution (cancel any pending request).
            *  scheduleAction('cancel') will cancel the request.
            */
            if (this.tree.timer) {
                clearTimeout(this.tree.timer);
                this.tree.logDebug("clearTimeout(%o)", this.tree.timer);
            }
            var self = this; // required for closures
            switch (mode) {
                case "cancel":
                    // Simply made sure that timer was cleared
                    break;
                case "expand":
                    this.tree.timer = setTimeout(function () {
                        self.tree.logDebug("setTimeout: trigger expand");
                        self.expand(true);
                    }, ms);
                    break;
                case "activate":
                    this.tree.timer = setTimeout(function () {
                        self.tree.logDebug("setTimeout: trigger activate");
                        self.activate();
                    }, ms);
                    break;
                default:
                    throw "Invalid mode " + mode;
            }
            this.tree.logDebug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer);
        },

        toggleExpand: function () {
            this.expand(!this.bExpanded);
        },

        collapseSiblings: function () {
            if (this.parent === null) {
                return;
            }
            var ac = this.parent.childList;
            for (var i = 0, l = ac.length; i < l; i++) {
                if (ac[i] !== this && ac[i].bExpanded) {
                    ac[i]._expand(false);
                }
            }
        },

        _onClick: function (event) {
            //		this.tree.logDebug("dtnode.onClick(" + event.type + "): dtnode:" + this + ", button:" + event.button + ", which: " + event.which);
            var targetType = this.getEventTargetType(event);
            if (targetType === "expander") {
                // Clicking the expander icon always expands/collapses
                this.toggleExpand();
                this.focus(); // issue 95
            } else if (targetType === "checkbox") {
                // Clicking the checkbox always (de)selects
                this.toggleSelect();
                this.focus(); // issue 95
            } else {
                this._userActivate();
                var aTag = this.span.getElementsByTagName("a");
                if (aTag[0]) {
                    // issue 154
                    // TODO: check if still required on IE 9:
                    // Chrome and Safari don't focus the a-tag on click,
                    // but calling focus() seem to have problems on IE:
                    // http://code.google.com/p/dynatree/issues/detail?id=154
                    if (!$.browser.msie) {
                        aTag[0].focus();
                    }
                } else {
                    // 'noLink' option was set
                    return true;
                }
            }
            // Make sure that clicks stop, otherwise <a href='#'> jumps to the top
            event.preventDefault();
        },

        _onDblClick: function (event) {
            //		this.tree.logDebug("dtnode.onDblClick(" + event.type + "): dtnode:" + this + ", button:" + event.button + ", which: " + event.which);
        },

        _onKeydown: function (event) {
            //		this.tree.logDebug("dtnode.onKeydown(" + event.type + "): dtnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which);
            var handled = true,
			sib;
            //		alert("keyDown" + event.which);

            switch (event.which) {
                // charCodes:  
                //			case 43: // '+'  
                case 107: // '+'
                case 187: // '+' @ Chrome, Safari
                    if (!this.bExpanded) { this.toggleExpand(); }
                    break;
                //			case 45: // '-'  
                case 109: // '-'
                case 189: // '+' @ Chrome, Safari
                    if (this.bExpanded) { this.toggleExpand(); }
                    break;
                //~ case 42: // '*'  
                //~ break;  
                //~ case 47: // '/'  
                //~ break;  
                // case 13: // <enter>  
                // <enter> on a focused <a> tag seems to generate a click-event.  
                // this._userActivate();  
                // break;  
                case 32: // <space>
                    this._userActivate();
                    break;
                case 8: // <backspace>
                    if (this.parent) {
                        this.parent.focus();
                    }
                    break;
                case 37: // <left>
                    if (this.bExpanded) {
                        this.toggleExpand();
                        this.focus();
                        //				} else if( this.parent && (this.tree.options.rootVisible || this.parent.parent) ) {
                    } else if (this.parent && this.parent.parent) {
                        this.parent.focus();
                    }
                    break;
                case 39: // <right>
                    if (!this.bExpanded && (this.childList || this.data.isLazy)) {
                        this.toggleExpand();
                        this.focus();
                    } else if (this.childList) {
                        this.childList[0].focus();
                    }
                    break;
                case 38: // <up>
                    sib = this.getPrevSibling();
                    while (sib && sib.bExpanded && sib.childList) {
                        sib = sib.childList[sib.childList.length - 1];
                    }
                    //				if( !sib && this.parent && (this.tree.options.rootVisible || this.parent.parent) )
                    if (!sib && this.parent && this.parent.parent) {
                        sib = this.parent;
                    }
                    if (sib) {
                        sib.focus();
                    }
                    break;
                case 40: // <down>
                    if (this.bExpanded && this.childList) {
                        sib = this.childList[0];
                    } else {
                        var parents = this._parentList(false, true);
                        for (var i = parents.length - 1; i >= 0; i--) {
                            sib = parents[i].getNextSibling();
                            if (sib) { break; }
                        }
                    }
                    if (sib) {
                        sib.focus();
                    }
                    break;
                default:
                    handled = false;
            }
            // Return false, if handled, to prevent default processing
            //		return !handled;
            if (handled) {
                event.preventDefault();
            }
        },

        _onKeypress: function (event) {
            // onKeypress is only hooked to allow user callbacks.
            // We don't process it, because IE and Safari don't fire keypress for cursor keys.
            //		this.tree.logDebug("dtnode.onKeypress(" + event.type + "): dtnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which);
        },

        _onFocus: function (event) {
            // Handles blur and focus events.
            //		this.tree.logDebug("dtnode.onFocus(%o): %o", event, this);
            var opts = this.tree.options;
            if (event.type == "blur" || event.type == "focusout") {
                if (opts.onBlur) {
                    opts.onBlur.call(this.tree, this);
                }
                if (this.tree.tnFocused) {
                    $(this.tree.tnFocused.span).removeClass(opts.classNames.focused);
                }
                this.tree.tnFocused = null;
                if (opts.persist) {
                    $.cookie(opts.cookieId + "-focus", "", opts.cookie);
                }
            } else if (event.type == "focus" || event.type == "focusin") {
                // Fix: sometimes the blur event is not generated
                if (this.tree.tnFocused && this.tree.tnFocused !== this) {
                    this.tree.logDebug("dtnode.onFocus: out of sync: curFocus: %o", this.tree.tnFocused);
                    $(this.tree.tnFocused.span).removeClass(opts.classNames.focused);
                }
                this.tree.tnFocused = this;
                if (opts.onFocus) {
                    opts.onFocus.call(this.tree, this);
                }
                $(this.tree.tnFocused.span).addClass(opts.classNames.focused);
                if (opts.persist) {
                    $.cookie(opts.cookieId + "-focus", this.data.key, opts.cookie);
                }
            }
            // TODO: return anything?
            //		return false;
        },

        visit: function (fn, includeSelf) {
            // Call fn(node) for all child nodes. Stop iteration, if fn() returns false.
            var res = true;
            if (includeSelf === true) {
                res = fn(this);
                if (res === false || res == "skip") {
                    return res;
                }
            }
            if (this.childList) {
                for (var i = 0, l = this.childList.length; i < l; i++) {
                    res = this.childList[i].visit(fn, true);
                    if (res === false) {
                        break;
                    }
                }
            }
            return res;
        },

        visitParents: function (fn, includeSelf) {
            // Visit parent nodes (bottom up)
            if (includeSelf && fn(this) === false) {
                return false;
            }
            var p = this.parent;
            while (p) {
                if (fn(p) === false) {
                    return false;
                }
                p = p.parent;
            }
            return true;
        },

        remove: function () {
            // Remove this node
            //		this.tree.logDebug ("%s.remove()", this);
            if (this === this.tree.root) {
                throw "Cannot remove system root";
            }
            return this.parent.removeChild(this);
        },

        removeChild: function (tn) {
            // Remove tn from list of direct children.
            var ac = this.childList;
            if (ac.length == 1) {
                if (tn !== ac[0]) {
                    throw "removeChild: invalid child";
                }
                return this.removeChildren();
            }
            if (tn === this.tree.activeNode) {
                tn.deactivate();
            }
            if (this.tree.options.persist) {
                if (tn.bSelected) {
                    this.tree.persistence.clearSelect(tn.data.key);
                }
                if (tn.bExpanded) {
                    this.tree.persistence.clearExpand(tn.data.key);
                }
            }
            tn.removeChildren(true);
            //		this.div.removeChild(tn.div);
            this.ul.removeChild(tn.li);
            for (var i = 0, l = ac.length; i < l; i++) {
                if (ac[i] === tn) {
                    this.childList.splice(i, 1);
                    //				delete tn;  // JSLint complained
                    break;
                }
            }
        },

        removeChildren: function (isRecursiveCall, retainPersistence) {
            // Remove all child nodes (more efficiently than recursive remove())
            this.tree.logDebug("%s.removeChildren(%o)", this, isRecursiveCall);
            var tree = this.tree;
            var ac = this.childList;
            if (ac) {
                for (var i = 0, l = ac.length; i < l; i++) {
                    var tn = ac[i];
                    if (tn === tree.activeNode && !retainPersistence) {
                        tn.deactivate();
                    }
                    if (this.tree.options.persist && !retainPersistence) {
                        if (tn.bSelected) {
                            this.tree.persistence.clearSelect(tn.data.key);
                        }
                        if (tn.bExpanded) {
                            this.tree.persistence.clearExpand(tn.data.key);
                        }
                    }
                    tn.removeChildren(true, retainPersistence);
                    if (this.ul) {
                        this.ul.removeChild(tn.li);
                    }
                    /*
                    try{
                    this.ul.removeChild(tn.li);
                    }catch(e){
                    this.tree.logDebug("%s.removeChildren: couldnt remove LI", this, e);
                    }
                    */
                    //				delete tn;  JSLint complained
                }
                // Set to 'null' which is interpreted as 'not yet loaded' for lazy
                // nodes
                this.childList = null;
            }
            if (!isRecursiveCall) {
                //			this._expand(false);
                //			this.isRead = false;
                this.isLoading = false;
                this.render();
            }
        },

        setTitle: function (title) {
            this.fromDict({ title: title });
        },

        reload: function (force) {
            throw "Use reloadChildren() instead";
        },

        reloadChildren: function (callback) {
            // Reload lazy content (expansion state is maintained).
            if (this.parent === null) {
                throw "Use tree.reload() instead";
            } else if (!this.data.isLazy) {
                throw "node.reloadChildren() requires lazy nodes.";
            }
            // appendAjax triggers 'nodeLoaded' event.
            // We listen to this, if a callback was passed to reloadChildren
            if (callback) {
                var self = this;
                var eventType = "nodeLoaded.dynatree." + this.tree.$tree.attr("id")
				+ "." + this.data.key;
                this.tree.$tree.bind(eventType, function (e, node, isOk) {
                    self.tree.$tree.unbind(eventType);
                    self.tree.logDebug("loaded %o, %o, %o", e, node, isOk);
                    if (node !== self) {
                        throw "got invalid load event";
                    }
                    callback.call(self.tree, node, isOk);
                });
            }
            // The expansion state is maintained
            this.removeChildren();
            this._loadContent();
            //		if( this.bExpanded ) {
            //			// Remove children first, to prevent effects being applied
            //			this.removeChildren();
            //			// then force re-expand to trigger lazy loading
            ////			this.expand(false);
            ////			this.expand(true);
            //			this._loadContent();
            //		} else {
            //			this.removeChildren();
            //			this._loadContent();
            //		}
        },

        /**
        * Make sure the node with a given key path is available in the tree.
        */
        _loadKeyPath: function (keyPath, callback) {
            var tree = this.tree;
            tree.logDebug("%s._loadKeyPath(%s)", this, keyPath);
            if (keyPath === "") {
                throw "Key path must not be empty";
            }
            var segList = keyPath.split(tree.options.keyPathSeparator);
            if (segList[0] === "") {
                throw "Key path must be relative (don't start with '/')";
            }
            var seg = segList.shift();

            for (var i = 0, l = this.childList.length; i < l; i++) {
                var child = this.childList[i];
                if (child.data.key === seg) {
                    if (segList.length === 0) {
                        // Found the end node
                        callback.call(tree, child, "ok");

                    } else if (child.data.isLazy && (child.childList === null || child.childList === undefined)) {
                        tree.logDebug("%s._loadKeyPath(%s) -> reloading %s...", this, keyPath, child);
                        var self = this;
                        child.reloadChildren(function (node, isOk) {
                            // After loading, look for direct child with that key
                            if (isOk) {
                                tree.logDebug("%s._loadKeyPath(%s) -> reloaded %s.", node, keyPath, node);
                                callback.call(tree, child, "loaded");
                                node._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback);
                            } else {
                                tree.logWarning("%s._loadKeyPath(%s) -> reloadChildren() failed.", self, keyPath);
                                callback.call(tree, child, "error");
                            }
                        }); // Note: this line gives a JSLint warning (Don't make functions within a loop)
                        // we can ignore it, since it will only be exectuted once, the the loop is ended
                        // See also http://stackoverflow.com/questions/3037598/how-to-get-around-the-jslint-error-dont-make-functions-within-a-loop
                    } else {
                        callback.call(tree, child, "loaded");
                        // Look for direct child with that key
                        child._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback);
                    }
                    return;
                }
            }
            // Could not find key
            tree.logWarning("Node not found: " + seg);
            return;
        },

        resetLazy: function () {
            // Discard lazy content.
            if (this.parent === null) {
                throw "Use tree.reload() instead";
            } else if (!this.data.isLazy) {
                throw "node.resetLazy() requires lazy nodes.";
            }
            this.expand(false);
            this.removeChildren();
        },

        _addChildNode: function (dtnode, beforeNode) {
            /**
            * Internal function to add one single DynatreeNode as a child.
            *
            */
            var tree = this.tree,
			opts = tree.options,
			pers = tree.persistence;

            //		tree.logDebug("%s._addChildNode(%o)", this, dtnode);

            // --- Update and fix dtnode attributes if necessary
            dtnode.parent = this;
            //		if( beforeNode && (beforeNode.parent !== this || beforeNode === dtnode ) )
            //			throw "<beforeNode> must be another child of <this>";

            // --- Add dtnode as a child
            if (this.childList === null) {
                this.childList = [];
            } else if (!beforeNode) {
                // Fix 'lastsib'
                if (this.childList.length > 0) {
                    $(this.childList[this.childList.length - 1].span).removeClass(opts.classNames.lastsib);
                }
            }
            if (beforeNode) {
                var iBefore = $.inArray(beforeNode, this.childList);
                if (iBefore < 0) {
                    throw "<beforeNode> must be a child of <this>";
                }
                this.childList.splice(iBefore, 0, dtnode);
            } else {
                // Append node
                this.childList.push(dtnode);
            }

            // --- Handle persistence
            // Initial status is read from cookies, if persistence is active and
            // cookies are already present.
            // Otherwise the status is read from the data attributes and then persisted.
            var isInitializing = tree.isInitializing();
            if (opts.persist && pers.cookiesFound && isInitializing) {
                // Init status from cookies
                //			tree.logDebug("init from cookie, pa=%o, dk=%o", pers.activeKey, dtnode.data.key);
                if (pers.activeKey === dtnode.data.key) {
                    tree.activeNode = dtnode;
                }
                if (pers.focusedKey === dtnode.data.key) {
                    tree.focusNode = dtnode;
                }
                dtnode.bExpanded = ($.inArray(dtnode.data.key, pers.expandedKeyList) >= 0);
                dtnode.bSelected = ($.inArray(dtnode.data.key, pers.selectedKeyList) >= 0);
                //			tree.logDebug("    key=%o, bSelected=%o", dtnode.data.key, dtnode.bSelected);
            } else {
                // Init status from data (Note: we write the cookies after the init phase)
                //			tree.logDebug("init from data");
                if (dtnode.data.activate) {
                    tree.activeNode = dtnode;
                    if (opts.persist) {
                        pers.activeKey = dtnode.data.key;
                    }
                }
                if (dtnode.data.focus) {
                    tree.focusNode = dtnode;
                    if (opts.persist) {
                        pers.focusedKey = dtnode.data.key;
                    }
                }
                dtnode.bExpanded = (dtnode.data.expand === true); // Collapsed by default
                if (dtnode.bExpanded && opts.persist) {
                    pers.addExpand(dtnode.data.key);
                }
                dtnode.bSelected = (dtnode.data.select === true); // Deselected by default
                /*
                Doesn't work, cause pers.selectedKeyList may be null
                if( dtnode.bSelected && opts.selectMode==1
                && pers.selectedKeyList && pers.selectedKeyList.length>0 ) {
                tree.logWarning("Ignored multi-selection in single-mode for %o", dtnode);
                dtnode.bSelected = false; // Fixing bad input data (multi selection for mode:1)
                }
                */
                if (dtnode.bSelected && opts.persist) {
                    pers.addSelect(dtnode.data.key);
                }
            }

            // Always expand, if it's below minExpandLevel
            //		tree.logDebug ("%s._addChildNode(%o), l=%o", this, dtnode, dtnode.getLevel());
            if (opts.minExpandLevel >= dtnode.getLevel()) {
                //			tree.logDebug ("Force expand for %o", dtnode);
                this.bExpanded = true;
            }

            // In multi-hier mode, update the parents selection state
            // issue #82: only if not initializing, because the children may not exist yet
            //		if( !dtnode.data.isStatusNode && opts.selectMode==3 && !isInitializing )
            //			dtnode._fixSelectionState();

            // In multi-hier mode, update the parents selection state
            if (dtnode.bSelected && opts.selectMode == 3) {
                var p = this;
                while (p) {
                    if (!p.hasSubSel) {
                        p._setSubSel(true);
                    }
                    p = p.parent;
                }
            }
            // render this node and the new child
            if (tree.bEnableUpdate) {
                this.render();
            }
            return dtnode;
        },

        addChild: function (obj, beforeNode) {
            /**
            * Add a node object as child.
            *
            * This should be the only place, where a DynaTreeNode is constructed!
            * (Except for the root node creation in the tree constructor)
            *
            * @param obj A JS object (may be recursive) or an array of those.
            * @param {DynaTreeNode} beforeNode (optional) sibling node.
            *
            * Data format: array of node objects, with optional 'children' attributes.
            * [
            *	{ title: "t1", isFolder: true, ... }
            *	{ title: "t2", isFolder: true, ...,
            *		children: [
            *			{title: "t2.1", ..},
            *			{..}
            *			]
            *	}
            * ]
            * A simple object is also accepted instead of an array.
            *
            */
            //		this.tree.logDebug("%s.addChild(%o, %o)", this, obj, beforeNode);
            if (typeof (obj) == "string") {
                throw "Invalid data type for " + obj;
            } else if (!obj || obj.length === 0) { // Passed null or undefined or empty array
                return;
            } else if (obj instanceof DynaTreeNode) {
                return this._addChildNode(obj, beforeNode);
            }

            if (!obj.length) { // Passed a single data object
                obj = [obj];
            }
            var prevFlag = this.tree.enableUpdate(false);

            var tnFirst = null;
            for (var i = 0, l = obj.length; i < l; i++) {
                var data = obj[i];
                var dtnode = this._addChildNode(new DynaTreeNode(this, this.tree, data), beforeNode);
                if (!tnFirst) {
                    tnFirst = dtnode;
                }
                // Add child nodes recursively
                if (data.children) {
                    dtnode.addChild(data.children, null);
                }
            }
            this.tree.enableUpdate(prevFlag);
            return tnFirst;
        },

        append: function (obj) {
            this.tree.logWarning("node.append() is deprecated (use node.addChild() instead).");
            return this.addChild(obj, null);
        },

        appendAjax: function (ajaxOptions) {
            var self = this;
            this.removeChildren(false, true);
            this.setLazyNodeStatus(DTNodeStatus_Loading);
            // Debug feature: force a delay, to simulate slow loading...
            if (ajaxOptions.debugLazyDelay) {
                var ms = ajaxOptions.debugLazyDelay;
                ajaxOptions.debugLazyDelay = 0;
                this.tree.logInfo("appendAjax: waiting for debugLazyDelay " + ms);
                setTimeout(function () { self.appendAjax(ajaxOptions); }, ms);
                return;
            }
            // Ajax option inheritance: $.ajaxSetup < $.ui.dynatree.prototype.options.ajaxDefaults < tree.options.ajaxDefaults < ajaxOptions
            var orgSuccess = ajaxOptions.success;
            var orgError = ajaxOptions.error;
            var eventType = "nodeLoaded.dynatree." + this.tree.$tree.attr("id")
			+ "." + this.data.key;
            var options = $.extend({}, this.tree.options.ajaxDefaults, ajaxOptions, {
                success: function (data, textStatus) {
                    // <this> is the request options
                    //				self.tree.logDebug("appendAjax().success");
                    var prevPhase = self.tree.phase;
                    self.tree.phase = "init";
                    // postProcess is similar to the standard dataFilter hook,
                    // but it is also called for JSONP
                    if (options.postProcess) {
                        data = options.postProcess.call(this, data, this.dataType);
                    }
                    // Process ASPX WebMethod JSON object inside "d" property
                    // http://code.google.com/p/dynatree/issues/detail?id=202
                    else if (data && data.hasOwnProperty("d")) {
                        data = data.d;
                    }
                    if (!$.isArray(data) || data.length !== 0) {
                        self.addChild(data, null);
                    }
                    self.tree.phase = "postInit";
                    if (orgSuccess) {
                        orgSuccess.call(options, self, data, textStatus);
                    }
                    self.tree.logDebug("trigger " + eventType);
                    self.tree.$tree.trigger(eventType, [self, true]);
                    self.tree.phase = prevPhase;
                    // This should be the last command, so node.isLoading is true
                    // while the callbacks run
                    self.setLazyNodeStatus(DTNodeStatus_Ok);
                    if ($.isArray(data) && data.length === 0) {
                        // Set to [] which is interpreted as 'no children' for lazy
                        // nodes
                        self.childList = [];
                        self.render();
                    }
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    // <this> is the request options
                    self.tree.logWarning("appendAjax failed:", textStatus, ":\n", XMLHttpRequest, "\n", errorThrown);
                    if (orgError) {
                        orgError.call(options, self, XMLHttpRequest, textStatus, errorThrown);
                    }
                    self.tree.$tree.trigger(eventType, [self, false]);
                    self.setLazyNodeStatus(DTNodeStatus_Error, { info: textStatus, tooltip: "" + errorThrown });
                }
            });
            $.ajax(options);
        },

        move: function (targetNode, mode) {
            /**Move this node to targetNode.
            *  mode 'child': append this node as last child of targetNode.
            *                This is the default. To be compatble with the D'n'd
            *                hitMode, we also accept 'over'.
            *  mode 'before': add this node as sibling before targetNode.
            *  mode 'after': add this node as sibling after targetNode.
            */
            var pos;
            if (this === targetNode) {
                return;
            }
            if (!this.parent) {
                throw "Cannot move system root";
            }
            if (mode === undefined || mode == "over") {
                mode = "child";
            }
            var prevParent = this.parent;
            var targetParent = (mode === "child") ? targetNode : targetNode.parent;
            if (targetParent.isDescendantOf(this)) {
                throw "Cannot move a node to it's own descendant";
            }
            // Unlink this node from current parent
            if (this.parent.childList.length == 1) {
                this.parent.childList = null;
                this.parent.bExpanded = false;
            } else {
                pos = $.inArray(this, this.parent.childList);
                if (pos < 0) {
                    throw "Internal error";
                }
                this.parent.childList.splice(pos, 1);
            }
            // Remove from source DOM parent
            this.parent.ul.removeChild(this.li);

            // Insert this node to target parent's child list
            this.parent = targetParent;
            if (targetParent.hasChildren()) {
                switch (mode) {
                    case "child":
                        // Append to existing target children
                        targetParent.childList.push(this);
                        break;
                    case "before":
                        // Insert this node before target node
                        pos = $.inArray(targetNode, targetParent.childList);
                        if (pos < 0) {
                            throw "Internal error";
                        }
                        targetParent.childList.splice(pos, 0, this);
                        break;
                    case "after":
                        // Insert this node after target node
                        pos = $.inArray(targetNode, targetParent.childList);
                        if (pos < 0) {
                            throw "Internal error";
                        }
                        targetParent.childList.splice(pos + 1, 0, this);
                        break;
                    default:
                        throw "Invalid mode " + mode;
                }
            } else {
                targetParent.childList = [this];
            }
            // Parent has no <ul> tag yet:
            if (!targetParent.ul) {
                // This is the parent's first child: create UL tag
                // (Hidden, because it will be
                targetParent.ul = document.createElement("ul");
                targetParent.ul.style.display = "none";
                targetParent.li.appendChild(targetParent.ul);
            }
            // Add to target DOM parent
            targetParent.ul.appendChild(this.li);

            if (this.tree !== targetNode.tree) {
                // Fix node.tree for all source nodes
                this.visit(function (node) {
                    node.tree = targetNode.tree;
                }, null, true);
                throw "Not yet implemented.";
            }
            // TODO: fix selection state
            // TODO: fix active state
            if (!prevParent.isDescendantOf(targetParent)) {
                prevParent.render();
            }
            if (!targetParent.isDescendantOf(prevParent)) {
                targetParent.render();
            }
            //		this.tree.redraw();
            /*
            var tree = this.tree;
            var opts = tree.options;
            var pers = tree.persistence;


            // Always expand, if it's below minExpandLevel
            //		tree.logDebug ("%s._addChildNode(%o), l=%o", this, dtnode, dtnode.getLevel());
            if ( opts.minExpandLevel >= dtnode.getLevel() ) {
            //			tree.logDebug ("Force expand for %o", dtnode);
            this.bExpanded = true;
            }

            // In multi-hier mode, update the parents selection state
            // issue #82: only if not initializing, because the children may not exist yet
            //		if( !dtnode.data.isStatusNode && opts.selectMode==3 && !isInitializing )
            //			dtnode._fixSelectionState();

            // In multi-hier mode, update the parents selection state
            if( dtnode.bSelected && opts.selectMode==3 ) {
            var p = this;
            while( p ) {
            if( !p.hasSubSel )
            p._setSubSel(true);
            p = p.parent;
            }
            }
            // render this node and the new child
            if ( tree.bEnableUpdate )
            this.render();

            return dtnode;

            */
        },

        // --- end of class
        lastentry: undefined
    };

    /*************************************************************************
    * class DynaTreeStatus
    */

    var DynaTreeStatus = Class.create();


    DynaTreeStatus._getTreePersistData = function (cookieId, cookieOpts) {
        // Static member: Return persistence information from cookies
        var ts = new DynaTreeStatus(cookieId, cookieOpts);
        ts.read();
        return ts.toDict();
    };
    // Make available in global scope
    getDynaTreePersistData = DynaTreeStatus._getTreePersistData; // TODO: deprecated


    DynaTreeStatus.prototype = {
        // Constructor
        initialize: function (cookieId, cookieOpts) {
            //		this._log("DynaTreeStatus: initialize");
            if (cookieId === undefined) {
                cookieId = $.ui.dynatree.prototype.options.cookieId;
            }
            cookieOpts = $.extend({}, $.ui.dynatree.prototype.options.cookie, cookieOpts);

            this.cookieId = cookieId;
            this.cookieOpts = cookieOpts;
            this.cookiesFound = undefined;
            this.activeKey = null;
            this.focusedKey = null;
            this.expandedKeyList = null;
            this.selectedKeyList = null;
        },
        // member functions
        _log: function (msg) {
            //	this.logDebug("_changeNodeList(%o): nodeList:%o, idx:%o", mode, nodeList, idx);
            Array.prototype.unshift.apply(arguments, ["debug"]);
            _log.apply(this, arguments);
        },
        read: function () {
            //		this._log("DynaTreeStatus: read");
            // Read or init cookies.
            this.cookiesFound = false;

            var cookie = $.cookie(this.cookieId + "-active");
            this.activeKey = (cookie === null) ? "" : cookie;
            if (cookie !== null) {
                this.cookiesFound = true;
            }
            cookie = $.cookie(this.cookieId + "-focus");
            this.focusedKey = (cookie === null) ? "" : cookie;
            if (cookie !== null) {
                this.cookiesFound = true;
            }
            cookie = $.cookie(this.cookieId + "-expand");
            this.expandedKeyList = (cookie === null) ? [] : cookie.split(",");
            if (cookie !== null) {
                this.cookiesFound = true;
            }
            cookie = $.cookie(this.cookieId + "-select");
            this.selectedKeyList = (cookie === null) ? [] : cookie.split(",");
            if (cookie !== null) {
                this.cookiesFound = true;
            }
        },
        write: function () {
            //		this._log("DynaTreeStatus: write");
            $.cookie(this.cookieId + "-active", (this.activeKey === null) ? "" : this.activeKey, this.cookieOpts);
            $.cookie(this.cookieId + "-focus", (this.focusedKey === null) ? "" : this.focusedKey, this.cookieOpts);
            $.cookie(this.cookieId + "-expand", (this.expandedKeyList === null) ? "" : this.expandedKeyList.join(","), this.cookieOpts);
            $.cookie(this.cookieId + "-select", (this.selectedKeyList === null) ? "" : this.selectedKeyList.join(","), this.cookieOpts);
        },
        addExpand: function (key) {
            //		this._log("addExpand(%o)", key);
            if ($.inArray(key, this.expandedKeyList) < 0) {
                this.expandedKeyList.push(key);
                $.cookie(this.cookieId + "-expand", this.expandedKeyList.join(","), this.cookieOpts);
            }
        },
        clearExpand: function (key) {
            //		this._log("clearExpand(%o)", key);
            var idx = $.inArray(key, this.expandedKeyList);
            if (idx >= 0) {
                this.expandedKeyList.splice(idx, 1);
                $.cookie(this.cookieId + "-expand", this.expandedKeyList.join(","), this.cookieOpts);
            }
        },
        addSelect: function (key) {
            //		this._log("addSelect(%o)", key);
            if ($.inArray(key, this.selectedKeyList) < 0) {
                this.selectedKeyList.push(key);
                $.cookie(this.cookieId + "-select", this.selectedKeyList.join(","), this.cookieOpts);
            }
        },
        clearSelect: function (key) {
            //		this._log("clearSelect(%o)", key);
            var idx = $.inArray(key, this.selectedKeyList);
            if (idx >= 0) {
                this.selectedKeyList.splice(idx, 1);
                $.cookie(this.cookieId + "-select", this.selectedKeyList.join(","), this.cookieOpts);
            }
        },
        isReloading: function () {
            return this.cookiesFound === true;
        },
        toDict: function () {
            return {
                cookiesFound: this.cookiesFound,
                activeKey: this.activeKey,
                focusedKey: this.activeKey,
                expandedKeyList: this.expandedKeyList,
                selectedKeyList: this.selectedKeyList
            };
        },
        // --- end of class
        lastentry: undefined
    };


    /*************************************************************************
    * class DynaTree
    */

    var DynaTree = Class.create();

    // --- Static members ----------------------------------------------------------

    DynaTree.version = "$Version: 1.2.0$";

    /*
    DynaTree._initTree = function() {
    };

    DynaTree._bind = function() {
    };
    */
    //--- Class members ------------------------------------------------------------

    DynaTree.prototype = {
        // Constructor
        //	initialize: function(divContainer, options) {
        initialize: function ($widget) {
            // instance members
            this.phase = "init";
            this.$widget = $widget;
            this.options = $widget.options;
            this.$tree = $widget.element;
            this.timer = null;
            // find container element
            this.divTree = this.$tree.get(0);

            //		var parentPos = $(this.divTree).parent().offset();
            //		this.parentTop = parentPos.top;
            //		this.parentLeft = parentPos.left;

            _initDragAndDrop(this);
        },

        // member functions

        _load: function (callback) {
            var $widget = this.$widget;
            var opts = this.options,
			self = this;
            this.bEnableUpdate = true;
            this._nodeCount = 1;
            this.activeNode = null;
            this.focusNode = null;

            // Some deprecation warnings to help with migration
            if (opts.rootVisible !== undefined) {
                this.logWarning("Option 'rootVisible' is no longer supported.");
            }
            if (opts.minExpandLevel < 1) {
                this.logWarning("Option 'minExpandLevel' must be >= 1.");
                opts.minExpandLevel = 1;
            }
            //		_log("warn", "jQuery.support.boxModel " + jQuery.support.boxModel);

            // If a 'options.classNames' dictionary was passed, still use defaults
            // for undefined classes:
            if (opts.classNames !== $.ui.dynatree.prototype.options.classNames) {
                opts.classNames = $.extend({}, $.ui.dynatree.prototype.options.classNames, opts.classNames);
            }
            if (opts.ajaxDefaults !== $.ui.dynatree.prototype.options.ajaxDefaults) {
                opts.ajaxDefaults = $.extend({}, $.ui.dynatree.prototype.options.ajaxDefaults, opts.ajaxDefaults);
            }
            if (opts.dnd !== $.ui.dynatree.prototype.options.dnd) {
                opts.dnd = $.extend({}, $.ui.dynatree.prototype.options.dnd, opts.dnd);
            }
            // Guess skin path, if not specified
            if (!opts.imagePath) {
                $("script").each(function () {
                    var _rexDtLibName = /.*dynatree[^\/]*\.js$/i;
                    if (this.src.search(_rexDtLibName) >= 0) {
                        if (this.src.indexOf("/") >= 0) { // issue #47
                            opts.imagePath = this.src.slice(0, this.src.lastIndexOf("/")) + "/skin/";
                        } else {
                            opts.imagePath = "skin/";
                        }
                        self.logDebug("Guessing imagePath from '%s': '%s'", this.src, opts.imagePath);
                        return false; // first match
                    }
                });
            }

            this.persistence = new DynaTreeStatus(opts.cookieId, opts.cookie);
            if (opts.persist) {
                if (!$.cookie) {
                    _log("warn", "Please include jquery.cookie.js to use persistence.");
                }
                this.persistence.read();
            }
            this.logDebug("DynaTree.persistence: %o", this.persistence.toDict());

            // Cached tag strings
            this.cache = {
                tagEmpty: "<span class='" + opts.classNames.empty + "'></span>",
                tagVline: "<span class='" + opts.classNames.vline + "'></span>",
                tagExpander: "<span class='" + opts.classNames.expander + "'></span>",
                tagConnector: "<span class='" + opts.classNames.connector + "'></span>",
                tagNodeIcon: "<span class='" + opts.classNames.nodeIcon + "'></span>",
                tagCheckbox: "<span class='" + opts.classNames.checkbox + "'></span>",
                lastentry: undefined
            };

            // Clear container, in case it contained some 'waiting' or 'error' text
            // for clients that don't support JS.
            // We don't do this however, if we try to load from an embedded UL element.
            if (opts.children || (opts.initAjax && opts.initAjax.url) || opts.initId) {
                $(this.divTree).empty();
            }
            var $ulInitialize = this.$tree.find(">ul:first").hide();

            // Create the root element
            this.tnRoot = new DynaTreeNode(null, this, {});
            this.tnRoot.bExpanded = true;
            this.tnRoot.render();
            this.divTree.appendChild(this.tnRoot.ul);

            var root = this.tnRoot;
            var isReloading = (opts.persist && this.persistence.isReloading());
            var isLazy = false;
            var prevFlag = this.enableUpdate(false);

            this.logDebug("Dynatree._load(): read tree structure...");

            // Init tree structure
            if (opts.children) {
                // Read structure from node array
                root.addChild(opts.children);

            } else if (opts.initAjax && opts.initAjax.url) {
                // Init tree from AJAX request
                isLazy = true;
                root.data.isLazy = true;
                this._reloadAjax(callback);

            } else if (opts.initId) {
                // Init tree from another UL element
                this._createFromTag(root, $("#" + opts.initId));

            } else {
                // Init tree from the first UL element inside the container <div>
                //			var $ul = this.$tree.find(">ul:first").hide();
                this._createFromTag(root, $ulInitialize);
                $ulInitialize.remove();
            }

            this._checkConsistency();
            // Fix part-sel flags
            if (!isLazy && opts.selectMode == 3) {
                root._updatePartSelectionState();
            }
            // Render html markup
            this.logDebug("Dynatree._load(): render nodes...");
            this.enableUpdate(prevFlag);

            // bind event handlers
            this.logDebug("Dynatree._load(): bind events...");
            this.$widget.bind();

            // --- Post-load processing
            this.logDebug("Dynatree._load(): postInit...");
            this.phase = "postInit";

            // In persist mode, make sure that cookies are written, even if they are empty
            if (opts.persist) {
                this.persistence.write();
            }
            // Set focus, if possible (this will also fire an event and write a cookie)
            if (this.focusNode && this.focusNode.isVisible()) {
                this.logDebug("Focus on init: %o", this.focusNode);
                this.focusNode.focus();
            }
            if (!isLazy) {
                if (opts.onPostInit) {
                    opts.onPostInit.call(this, isReloading, false);
                }
                if (callback) {
                    callback.call(this, "ok");
                }
            }
            this.phase = "idle";
        },

        _reloadAjax: function (callback) {
            // Reload
            var opts = this.options;
            if (!opts.initAjax || !opts.initAjax.url) {
                throw "tree.reload() requires 'initAjax' mode.";
            }
            var pers = this.persistence;
            var ajaxOpts = $.extend({}, opts.initAjax);
            // Append cookie info to the request
            //		this.logDebug("reloadAjax: key=%o, an.key:%o", pers.activeKey, this.activeNode?this.activeNode.data.key:"?");
            if (ajaxOpts.addActiveKey) {
                ajaxOpts.data.activeKey = pers.activeKey;
            }
            if (ajaxOpts.addFocusedKey) {
                ajaxOpts.data.focusedKey = pers.focusedKey;
            }
            if (ajaxOpts.addExpandedKeyList) {
                ajaxOpts.data.expandedKeyList = pers.expandedKeyList.join(",");
            }
            if (ajaxOpts.addSelectedKeyList) {
                ajaxOpts.data.selectedKeyList = pers.selectedKeyList.join(",");
            }
            // Set up onPostInit callback to be called when Ajax returns
            if (ajaxOpts.success) {
                this.logWarning("initAjax: success callback is ignored; use onPostInit instead.");
            }
            if (ajaxOpts.error) {
                this.logWarning("initAjax: error callback is ignored; use onPostInit instead.");
            }
            var isReloading = pers.isReloading();
            ajaxOpts.success = function (dtnode, data, textStatus) {
                if (opts.selectMode == 3) {
                    dtnode.tree.tnRoot._updatePartSelectionState();
                }
                if (opts.onPostInit) {
                    opts.onPostInit.call(dtnode.tree, isReloading, false);
                }
                if (callback) {
                    callback.call(dtnode.tree, "ok");
                }
            };
            ajaxOpts.error = function (dtnode, XMLHttpRequest, textStatus, errorThrown) {
                if (opts.onPostInit) {
                    opts.onPostInit.call(dtnode.tree, isReloading, true, XMLHttpRequest, textStatus, errorThrown);
                }
                if (callback) {
                    callback.call(dtnode.tree, "error", XMLHttpRequest, textStatus, errorThrown);
                }
            };
            //		}
            this.logDebug("Dynatree._init(): send Ajax request...");
            this.tnRoot.appendAjax(ajaxOpts);
        },

        toString: function () {
            //		return "DynaTree '" + this.options.title + "'";
            return "Dynatree '" + this.$tree.attr("id") + "'";
        },

        toDict: function () {
            return this.tnRoot.toDict(true);
        },

        serializeArray: function (stopOnParents) {
            // Return a JavaScript array of objects, ready to be encoded as a JSON
            // string for selected nodes
            var nodeList = this.getSelectedNodes(stopOnParents),
			name = this.$tree.attr("name") || this.$tree.attr("id"),
			arr = [];
            for (var i = 0, l = nodeList.length; i < l; i++) {
                arr.push({ name: name, value: nodeList[i].data.key });
            }
            return arr;
        },

        getPersistData: function () {
            return this.persistence.toDict();
        },

        logDebug: function (msg) {
            if (this.options.debugLevel >= 2) {
                Array.prototype.unshift.apply(arguments, ["debug"]);
                _log.apply(this, arguments);
            }
        },

        logInfo: function (msg) {
            if (this.options.debugLevel >= 1) {
                Array.prototype.unshift.apply(arguments, ["info"]);
                _log.apply(this, arguments);
            }
        },

        logWarning: function (msg) {
            Array.prototype.unshift.apply(arguments, ["warn"]);
            _log.apply(this, arguments);
        },

        isInitializing: function () {
            return (this.phase == "init" || this.phase == "postInit");
        },
        isReloading: function () {
            return (this.phase == "init" || this.phase == "postInit") && this.options.persist && this.persistence.cookiesFound;
        },
        isUserEvent: function () {
            return (this.phase == "userEvent");
        },

        redraw: function () {
            //		this.logDebug("dynatree.redraw()...");
            this.tnRoot.render(false, false);
            //		this.logDebug("dynatree.redraw() done.");
        },
        renderInvisibleNodes: function () {
            this.tnRoot.render(false, true);
        },
        reload: function (callback) {
            this._load(callback);
        },

        getRoot: function () {
            return this.tnRoot;
        },

        enable: function () {
            this.$widget.enable();
        },

        disable: function () {
            this.$widget.disable();
        },

        getNodeByKey: function (key) {
            // Search the DOM by element ID (assuming this is faster than traversing all nodes).
            // $("#...") has problems, if the key contains '.', so we use getElementById()
            var el = document.getElementById(this.options.idPrefix + key);
            if (el) {
                return el.dtnode ? el.dtnode : null;
            }
            // Not found in the DOM, but still may be in an unrendered part of tree
            var match = null;
            this.visit(function (node) {
                //			window.console.log("%s", node);
                if (node.data.key == key) {
                    match = node;
                    return false;
                }
            }, true);
            return match;
        },

        getActiveNode: function () {
            return this.activeNode;
        },

        reactivate: function (setFocus) {
            // Re-fire onQueryActivate and onActivate events.
            var node = this.activeNode;
            //		this.logDebug("reactivate %o", node);
            if (node) {
                this.activeNode = null; // Force re-activating
                node.activate();
                if (setFocus) {
                    node.focus();
                }
            }
        },

        getSelectedNodes: function (stopOnParents) {
            var nodeList = [];
            this.tnRoot.visit(function (node) {
                if (node.bSelected) {
                    nodeList.push(node);
                    if (stopOnParents === true) {
                        return "skip"; // stop processing this branch
                    }
                }
            });
            return nodeList;
        },

        activateKey: function (key) {
            var dtnode = (key === null) ? null : this.getNodeByKey(key);
            if (!dtnode) {
                if (this.activeNode) {
                    this.activeNode.deactivate();
                }
                this.activeNode = null;
                return null;
            }
            dtnode.focus();
            dtnode.activate();
            return dtnode;
        },

        loadKeyPath: function (keyPath, callback) {
            var segList = keyPath.split(this.options.keyPathSeparator);
            // Remove leading '/'
            if (segList[0] === "") {
                segList.shift();
            }
            // Remove leading system root key
            if (segList[0] == this.tnRoot.data.key) {
                this.logDebug("Removed leading root key.");
                segList.shift();
            }
            keyPath = segList.join(this.options.keyPathSeparator);
            return this.tnRoot._loadKeyPath(keyPath, callback);
        },

        selectKey: function (key, select) {
            var dtnode = this.getNodeByKey(key);
            if (!dtnode) {
                return null;
            }
            dtnode.select(select);
            return dtnode;
        },

        enableUpdate: function (bEnable) {
            if (this.bEnableUpdate == bEnable) {
                return bEnable;
            }
            this.bEnableUpdate = bEnable;
            if (bEnable) {
                this.redraw();
            }
            return !bEnable; // return previous value
        },

        count: function () {
            return this.tnRoot.countChildren();
        },

        visit: function (fn, includeRoot) {
            return this.tnRoot.visit(fn, includeRoot);
        },

        _createFromTag: function (parentTreeNode, $ulParent) {
            // Convert a <UL>...</UL> list into children of the parent tree node.
            var self = this;
            /*
            TODO: better?
            this.$lis = $("li:has(a[href])", this.element);
            this.$tabs = this.$lis.map(function() { return $("a", this)[0]; });
            */
            $ulParent.find(">li").each(function () {
                var $li = $(this),
				$liSpan = $li.find(">span:first"),
				$liA = $li.find(">a:first"),
				title,
				href = null,
				target = null,
				tooltip;
                if ($liSpan.length) {
                    // If a <li><span> tag is specified, use it literally.
                    title = $liSpan.html();
                } else if ($liA.length) {
                    title = $liA.html();
                    href = $liA.attr("href");
                    target = $liA.attr("target");
                    tooltip = $liA.attr("title");
                } else {
                    // If only a <li> tag is specified, use the trimmed string up to
                    // the next child <ul> tag.
                    title = $li.html();
                    var iPos = title.search(/<ul/i);
                    if (iPos >= 0) {
                        title = $.trim(title.substring(0, iPos));
                    } else {
                        title = $.trim(title);
                    }
                    //				self.logDebug("%o", title);
                }
                // Parse node options from ID, title and class attributes
                var data = {
                    title: title,
                    tooltip: tooltip,
                    isFolder: $li.hasClass("folder"),
                    isLazy: $li.hasClass("lazy"),
                    expand: $li.hasClass("expanded"),
                    select: $li.hasClass("selected"),
                    activate: $li.hasClass("active"),
                    focus: $li.hasClass("focused"),
                    noLink: $li.hasClass("noLink")
                };
                if (href) {
                    data.href = href;
                    data.target = target;
                }
                if ($li.attr("title")) {
                    data.tooltip = $li.attr("title"); // overrides <a title='...'>
                }
                if ($li.attr("id")) {
                    data.key = $li.attr("id");
                }
                // If a data attribute is present, evaluate as a JavaScript object
                if ($li.attr("data")) {
                    var dataAttr = $.trim($li.attr("data"));
                    if (dataAttr) {
                        if (dataAttr.charAt(0) != "{") {
                            dataAttr = "{" + dataAttr + "}";
                        }
                        try {
                            $.extend(data, eval("(" + dataAttr + ")"));
                        } catch (e) {
                            throw ("Error parsing node data: " + e + "\ndata:\n'" + dataAttr + "'");
                        }
                    }
                }
                var childNode = parentTreeNode.addChild(data);
                // Recursive reading of child nodes, if LI tag contains an UL tag
                var $ul = $li.find(">ul:first");
                if ($ul.length) {
                    self._createFromTag(childNode, $ul); // must use 'self', because 'this' is the each() context
                }
            });
        },

        _checkConsistency: function () {
            //		this.logDebug("tree._checkConsistency() NOT IMPLEMENTED - %o", this);
        },

        _setDndStatus: function (sourceNode, targetNode, helper, hitMode, accept) {
            // hitMode: 'after', 'before', 'over', 'out', 'start', 'stop'
            var $source = sourceNode ? $(sourceNode.span) : null,
			$target = $(targetNode.span);
            if (!this.$dndMarker) {
                this.$dndMarker = $("<div id='dynatree-drop-marker'></div>")
				.hide()
				.prependTo($(this.divTree).parent());
                //				.prependTo("body");
                //			logMsg("Creating marker: %o", this.$dndMarker);
            }
            /*
            if(hitMode === "start"){
            }
            if(hitMode === "stop"){
            //			sourceNode.removeClass("dynatree-drop-target");
            }
            */
            //		this.$dndMarker.attr("class", hitMode);
            if (hitMode === "after" || hitMode === "before" || hitMode === "over") {
                //			$source && $source.addClass("dynatree-drag-source");
                var pos = $target.offset();
                switch (hitMode) {
                    case "before":
                        this.$dndMarker.removeClass("dynatree-drop-after dynatree-drop-over");
                        this.$dndMarker.addClass("dynatree-drop-before");
                        pos.top -= 8;
                        break;
                    case "after":
                        this.$dndMarker.removeClass("dynatree-drop-before dynatree-drop-over");
                        this.$dndMarker.addClass("dynatree-drop-after");
                        pos.top += 8;
                        break;
                    default:
                        this.$dndMarker.removeClass("dynatree-drop-after dynatree-drop-before");
                        this.$dndMarker.addClass("dynatree-drop-over");
                        $target.addClass("dynatree-drop-target");
                        pos.left += 8;
                }
                //			logMsg("Creating marker: %o", this.$dndMarker);
                //			logMsg("    $target.offset=%o", $target);
                //			logMsg("    pos/$target.offset=%o", pos);
                //			logMsg("    $target.position=%o", $target.position());
                //			logMsg("    $target.offsetParent=%o, ot:%o", $target.offsetParent(), $target.offsetParent().offset());
                //			logMsg("    $(this.divTree).offset=%o", $(this.divTree).offset());
                //			logMsg("    $(this.divTree).parent=%o", $(this.divTree).parent());

                this.$dndMarker.offset({ left: pos.left, top: pos.top })
				.css({
				    "z-index": 1000
				})
				.show();
                //			helper.addClass("dynatree-drop-hover");
            } else {
                //			$source && $source.removeClass("dynatree-drag-source");
                $target.removeClass("dynatree-drop-target");
                this.$dndMarker.hide();
                //			helper.removeClass("dynatree-drop-hover");
            }
            if (hitMode === "after") {
                $target.addClass("dynatree-drop-after");
            } else {
                $target.removeClass("dynatree-drop-after");
            }
            if (hitMode === "before") {
                $target.addClass("dynatree-drop-before");
            } else {
                $target.removeClass("dynatree-drop-before");
            }
            if (accept === true) {
                if ($source) {
                    $source.addClass("dynatree-drop-accept");
                }
                $target.addClass("dynatree-drop-accept");
                helper.addClass("dynatree-drop-accept");
            } else {
                if ($source) {
                    $source.removeClass("dynatree-drop-accept");
                }
                $target.removeClass("dynatree-drop-accept");
                helper.removeClass("dynatree-drop-accept");
            }
            if (accept === false) {
                if ($source) {
                    $source.addClass("dynatree-drop-reject");
                }
                $target.addClass("dynatree-drop-reject");
                helper.addClass("dynatree-drop-reject");
            } else {
                if ($source) {
                    $source.removeClass("dynatree-drop-reject");
                }
                $target.removeClass("dynatree-drop-reject");
                helper.removeClass("dynatree-drop-reject");
            }
        },

        _onDragEvent: function (eventName, node, otherNode, event, ui, draggable) {
            /**
            * Handles drag'n'drop functionality.
            *
            * A standard jQuery drag-and-drop process may generate these calls:
            *
            * draggable helper():
            *     _onDragEvent("helper", sourceNode, null, event, null, null);
            * start:
            *     _onDragEvent("start", sourceNode, null, event, ui, draggable);
            * drag:
            *     _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
            *     _onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
            *     _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
            * stop:
            *     _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
            *     _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
            *     _onDragEvent("stop", sourceNode, null, event, ui, draggable);
            */
            //		if(eventName !== "over"){
            //			this.logDebug("tree._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this);
            //		}
            var opts = this.options;
            var dnd = this.options.dnd;
            var res = null;
            var nodeTag = $(node.span);
            var hitMode;

            switch (eventName) {
                case "helper":
                    // Only event and node argument is available
                    var helper = $("<div class='dynatree-drag-helper'><span class='dynatree-drag-helper-img' /></div>")
				.append($(event.target).closest('a').clone());
                    // Attach node reference to helper object
                    helper.data("dtSourceNode", node);
                    //			this.logDebug("helper.sourceNode=%o", helper.data("dtSourceNode"));
                    res = helper;
                    break;
                case "start":
                    if (node.isStatusNode()) {
                        res = false;
                    } else if (dnd.onDragStart) {
                        res = dnd.onDragStart(node);
                    }
                    if (res === false) {
                        this.logDebug("tree.onDragStart() cancelled");
                        //draggable._clear();
                        // NOTE: the return value seems to be ignored (drag is not canceled, when false is returned)
                        ui.helper.trigger("mouseup");
                        ui.helper.hide();
                    } else {
                        nodeTag.addClass("dynatree-drag-source");
                    }
                    break;
                case "enter":
                    res = dnd.onDragEnter ? dnd.onDragEnter(node, otherNode) : null;
                    res = {
                        over: (res !== false) && ((res === true) || (res === "over") || $.inArray("over", res) >= 0),
                        before: (res !== false) && ((res === true) || (res === "before") || $.inArray("before", res) >= 0),
                        after: (res !== false) && ((res === true) || (res === "after") || $.inArray("after", res) >= 0)
                    };
                    ui.helper.data("enterResponse", res);
                    //			this.logDebug("helper.enterResponse: %o", res);
                    break;
                case "over":
                    var enterResponse = ui.helper.data("enterResponse");
                    hitMode = null;
                    if (enterResponse === false) {
                        // Don't call onDragOver if onEnter returned false.
                        break;
                    } else if (typeof enterResponse === "string") {
                        // Use hitMode from onEnter if provided.
                        hitMode = enterResponse;
                    } else {
                        // Calculate hitMode from relative cursor position.
                        var nodeOfs = nodeTag.offset();
                        //				var relPos = { x: event.clientX - nodeOfs.left,
                        //							y: event.clientY - nodeOfs.top };
                        //				nodeOfs.top += this.parentTop;
                        //				nodeOfs.left += this.parentLeft;
                        var relPos = { x: event.pageX - nodeOfs.left,
                            y: event.pageY - nodeOfs.top
                        };
                        var relPos2 = { x: relPos.x / nodeTag.width(),
                            y: relPos.y / nodeTag.height()
                        };
                        //				this.logDebug("event.page: %s/%s", event.pageX, event.pageY);
                        //				this.logDebug("event.client: %s/%s", event.clientX, event.clientY);
                        //				this.logDebug("nodeOfs: %s/%s", nodeOfs.left, nodeOfs.top);
                        ////				this.logDebug("parent: %s/%s", this.parentLeft, this.parentTop);
                        //				this.logDebug("relPos: %s/%s", relPos.x, relPos.y);
                        //				this.logDebug("relPos2: %s/%s", relPos2.x, relPos2.y);
                        if (enterResponse.after && relPos2.y > 0.75) {
                            hitMode = "after";
                        } else if (!enterResponse.over && enterResponse.after && relPos2.y > 0.5) {
                            hitMode = "after";
                        } else if (enterResponse.before && relPos2.y <= 0.25) {
                            hitMode = "before";
                        } else if (!enterResponse.over && enterResponse.before && relPos2.y <= 0.5) {
                            hitMode = "before";
                        } else if (enterResponse.over) {
                            hitMode = "over";
                        }
                        // Prevent no-ops like 'before source node'
                        // TODO: these are no-ops when moving nodes, but not in copy mode
                        if (dnd.preventVoidMoves) {
                            if (node === otherNode) {
                                //						this.logDebug("    drop over source node prevented");
                                hitMode = null;
                            } else if (hitMode === "before" && otherNode && node === otherNode.getNextSibling()) {
                                //						this.logDebug("    drop after source node prevented");
                                hitMode = null;
                            } else if (hitMode === "after" && otherNode && node === otherNode.getPrevSibling()) {
                                //						this.logDebug("    drop before source node prevented");
                                hitMode = null;
                            } else if (hitMode === "over" && otherNode
							&& otherNode.parent === node && otherNode.isLastSibling()) {
                                //						this.logDebug("    drop last child over own parent prevented");
                                hitMode = null;
                            }
                        }
                        //				this.logDebug("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling());
                        ui.helper.data("hitMode", hitMode);
                    }
                    // Auto-expand node (only when 'over' the node, not 'before', or 'after')
                    if (hitMode === "over"
				&& dnd.autoExpandMS && node.hasChildren() !== false && !node.bExpanded) {
                        node.scheduleAction("expand", dnd.autoExpandMS);
                    }
                    if (hitMode && dnd.onDragOver) {
                        res = dnd.onDragOver(node, otherNode, hitMode);
                    }
                    this._setDndStatus(otherNode, node, ui.helper, hitMode, res !== false);
                    break;
                case "drop":
                    hitMode = ui.helper.data("hitMode");
                    if (hitMode && dnd.onDrop) {
                        dnd.onDrop(node, otherNode, hitMode, ui, draggable);
                    }
                    break;
                case "leave":
                    // Cancel pending expand request
                    node.scheduleAction("cancel");
                    ui.helper.data("enterResponse", null);
                    ui.helper.data("hitMode", null);
                    this._setDndStatus(otherNode, node, ui.helper, "out", undefined);
                    if (dnd.onDragLeave) {
                        dnd.onDragLeave(node, otherNode);
                    }
                    break;
                case "stop":
                    nodeTag.removeClass("dynatree-drag-source");
                    if (dnd.onDragStop) {
                        dnd.onDragStop(node);
                    }
                    break;
                default:
                    throw "Unsupported drag event: " + eventName;
            }
            return res;
        },

        cancelDrag: function () {
            var dd = $.ui.ddmanager.current;
            if (dd) {
                dd.cancel();
            }
        },

        // --- end of class
        lastentry: undefined
    };

    /*************************************************************************
    * Widget $(..).dynatree
    */

    $.widget("ui.dynatree", {
        /*
        init: function() {
        // ui.core 1.6 renamed init() to _init(): this stub assures backward compatibility
        _log("warn", "ui.dynatree.init() was called; you should upgrade to jquery.ui.core.js v1.8 or higher.");
        return this._init();
        },
        */
        _init: function () {
            if (parseFloat($.ui.version) < 1.8) {
                // jquery.ui.core 1.8 renamed _init() to _create(): this stub assures backward compatibility
                if (this.options.debugLevel >= 0) {
                    _log("warn", "ui.dynatree._init() was called; you should upgrade to jquery.ui.core.js v1.8 or higher.");
                }
                return this._create();
            }
            // jquery.ui.core 1.8 still uses _init() to perform "default functionality"
            if (this.options.debugLevel >= 2) {
                _log("debug", "ui.dynatree._init() was called; no current default functionality.");
            }
        },

        _create: function () {
            var opts = this.options;
            if (opts.debugLevel >= 1) {
                logMsg("Dynatree._create(): version='%s', debugLevel=%o.", $.ui.dynatree.version, this.options.debugLevel);
            }
            // The widget framework supplies this.element and this.options.
            this.options.event += ".dynatree"; // namespace event

            var divTree = this.element.get(0);
            /*		// Clear container, in case it contained some 'waiting' or 'error' text
            // for clients that don't support JS
            if( opts.children || (opts.initAjax && opts.initAjax.url) || opts.initId )
            $(divTree).empty();
            */
            // Create the DynaTree object
            this.tree = new DynaTree(this);
            this.tree._load();
            this.tree.logDebug("Dynatree._init(): done.");
        },

        bind: function () {
            // Prevent duplicate binding
            this.unbind();

            var eventNames = "click.dynatree dblclick.dynatree";
            if (this.options.keyboard) {
                // Note: leading ' '!
                eventNames += " keypress.dynatree keydown.dynatree";
            }
            this.element.bind(eventNames, function (event) {
                var dtnode = getDtNodeFromElement(event.target);
                if (!dtnode) {
                    return true;  // Allow bubbling of other events
                }
                var tree = dtnode.tree;
                var o = tree.options;
                tree.logDebug("event(%s): dtnode: %s", event.type, dtnode);
                var prevPhase = tree.phase;
                tree.phase = "userEvent";
                try {
                    switch (event.type) {
                        case "click":
                            return (o.onClick && o.onClick.call(tree, dtnode, event) === false) ? false : dtnode._onClick(event);
                        case "dblclick":
                            return (o.onDblClick && o.onDblClick.call(tree, dtnode, event) === false) ? false : dtnode._onDblClick(event);
                        case "keydown":
                            return (o.onKeydown && o.onKeydown.call(tree, dtnode, event) === false) ? false : dtnode._onKeydown(event);
                        case "keypress":
                            return (o.onKeypress && o.onKeypress.call(tree, dtnode, event) === false) ? false : dtnode._onKeypress(event);
                    }
                } catch (e) {
                    var _ = null; // issue 117
                    tree.logWarning("bind(%o): dtnode: %o, error: %o", event, dtnode, e);
                } finally {
                    tree.phase = prevPhase;
                }
            });

            // focus/blur don't bubble, i.e. are not delegated to parent <div> tags,
            // so we use the addEventListener capturing phase.
            // See http://www.howtocreate.co.uk/tutorials/javascript/domevents
            function __focusHandler(event) {
                // Handles blur and focus.
                // Fix event for IE:
                // doesn't pass JSLint:
                //			event = arguments[0] = $.event.fix( event || window.event );
                // what jQuery does:
                //			var args = jQuery.makeArray( arguments );
                //			event = args[0] = jQuery.event.fix( event || window.event );
                event = $.event.fix(event || window.event);
                var dtnode = getDtNodeFromElement(event.target);
                return dtnode ? dtnode._onFocus(event) : false;
            }
            var div = this.tree.divTree;
            if (div.addEventListener) {
                div.addEventListener("focus", __focusHandler, true);
                div.addEventListener("blur", __focusHandler, true);
            } else {
                div.onfocusin = div.onfocusout = __focusHandler;
            }
            // EVENTS
            // disable click if event is configured to something else
            //		if (!(/^click/).test(o.event))
            //			this.$tabs.bind("click.tabs", function() { return false; });

        },

        unbind: function () {
            this.element.unbind(".dynatree");
        },

        /* TODO: we could handle option changes during runtime here (maybe to re-render, ...)
        setData: function(key, value) {
        this.tree.logDebug("dynatree.setData('" + key + "', '" + value + "')");
        },
        */
        enable: function () {
            this.bind();
            // Call default disable(): remove -disabled from css:
            $.Widget.prototype.enable.apply(this, arguments);
        },

        disable: function () {
            this.unbind();
            // Call default disable(): add -disabled to css:
            $.Widget.prototype.disable.apply(this, arguments);
        },

        // --- getter methods (i.e. NOT returning a reference to $)
        getTree: function () {
            return this.tree;
        },

        getRoot: function () {
            return this.tree.getRoot();
        },

        getActiveNode: function () {
            return this.tree.getActiveNode();
        },

        getSelectedNodes: function () {
            return this.tree.getSelectedNodes();
        },

        // ------------------------------------------------------------------------
        lastentry: undefined
    });


    // The following methods return a value (thus breaking the jQuery call chain):
    if (parseFloat($.ui.version) < 1.8) {
        $.ui.dynatree.getter = "getTree getRoot getActiveNode getSelectedNodes";
    }

    /*******************************************************************************
    * Tools in ui.dynatree namespace
    */
    $.ui.dynatree.version = "$Version: 1.2.0$";

    /**
    * Return a DynaTreeNode object for a given DOM element
    */
    $.ui.dynatree.getNode = function (el) {
        if (el instanceof DynaTreeNode) {
            return el; // el already was a DynaTreeNode
        }
        // TODO: for some reason $el.parents("[dtnode]") does not work (jQuery 1.6.1)
        // maybe, because dtnode is a property, not an attribute
        var $el = el.selector === undefined ? $(el) : el,
        //		parent = $el.closest("[dtnode]"),
		parent = $el.parents("[dtnode]").first(),
		node;
        if (typeof parent.prop == "function") {
            node = parent.prop("dtnode");
        } else { // pre jQuery 1.6
            node = parent.attr("dtnode");
        }
        return node;
    }

    /**Return persistence information from cookies.*/
    $.ui.dynatree.getPersistData = DynaTreeStatus._getTreePersistData;

    /*******************************************************************************
    * Plugin default options:
    */
    $.ui.dynatree.prototype.options = {
        title: "Dynatree", // Tree's name (only used for debug outpu)
        minExpandLevel: 1, // 1: root node is not collapsible
        imagePath: null, // Path to a folder containing icons. Defaults to 'skin/' subdirectory.
        children: null, // Init tree structure from this object array.
        initId: null, // Init tree structure from a <ul> element with this ID.
        initAjax: null, // Ajax options used to initialize the tree strucuture.
        autoFocus: true, // Set focus to first child, when expanding or lazy-loading.
        keyboard: true, // Support keyboard navigation.
        persist: false, // Persist expand-status to a cookie
        autoCollapse: false, // Automatically collapse all siblings, when a node is expanded.
        clickFolderMode: 3, // 1:activate, 2:expand, 3:activate and expand
        activeVisible: true, // Make sure, active nodes are visible (expanded).
        checkbox: false, // Show checkboxes.
        selectMode: 2, // 1:single, 2:multi, 3:multi-hier
        fx: null, // Animations, e.g. null or { height: "toggle", duration: 200 }
        noLink: false, // Use <span> instead of <a> tags for all nodes
        // Low level event handlers: onEvent(dtnode, event): return false, to stop default processing
        onClick: null, // null: generate focus, expand, activate, select events.
        onDblClick: null, // (No default actions.)
        onKeydown: null, // null: generate keyboard navigation (focus, expand, activate).
        onKeypress: null, // (No default actions.)
        onFocus: null, // null: set focus to node.
        onBlur: null, // null: remove focus from node.

        // Pre-event handlers onQueryEvent(flag, dtnode): return false, to stop processing
        onQueryActivate: null, // Callback(flag, dtnode) before a node is (de)activated.
        onQuerySelect: null, // Callback(flag, dtnode) before a node is (de)selected.
        onQueryExpand: null, // Callback(flag, dtnode) before a node is expanded/collpsed.

        // High level event handlers
        onPostInit: null, // Callback(isReloading, isError) when tree was (re)loaded.
        onActivate: null, // Callback(dtnode) when a node is activated.
        onDeactivate: null, // Callback(dtnode) when a node is deactivated.
        onSelect: null, // Callback(flag, dtnode) when a node is (de)selected.
        onExpand: null, // Callback(flag, dtnode) when a node is expanded/collapsed.
        onLazyRead: null, // Callback(dtnode) when a lazy node is expanded for the first time.
        onCustomRender: null, // Callback(dtnode) before a node is rendered. Return a HTML string to override.
        onCreate: null, // Callback(dtnode, nodeSpan) after a node was rendered for the first time.
        onRender: null, // Callback(dtnode, nodeSpan) after a node was rendered.

        // Drag'n'drop support
        dnd: {
            // Make tree nodes draggable:
            onDragStart: null, // Callback(sourceNode), return true, to enable dnd
            onDragStop: null, // Callback(sourceNode)
            //		helper: null,
            // Make tree nodes accept draggables
            autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering.
            preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
            onDragEnter: null, // Callback(targetNode, sourceNode)
            onDragOver: null, // Callback(targetNode, sourceNode, hitMode)
            onDrop: null, // Callback(targetNode, sourceNode, hitMode)
            onDragLeave: null // Callback(targetNode, sourceNode)
        },
        ajaxDefaults: { // Used by initAjax option
            cache: false, // false: Append random '_' argument to the request url to prevent caching.
            dataType: "json" // Expect json format and pass json object to callbacks.
        },
        strings: {
            loading: "Loading&#8230;",
            loadError: "Load error!"
        },
        generateIds: false, // Generate id attributes like <span id='dynatree-id-KEY'>
        idPrefix: "dynatree-id-", // Used to generate node id's like <span id="dynatree-id-<key>">.
        keyPathSeparator: "/", // Used by node.getKeyPath() and tree.loadKeyPath().
        //    cookieId: "dynatree-cookie", // Choose a more unique name, to allow multiple trees.
        cookieId: "dynatree", // Choose a more unique name, to allow multiple trees.
        cookie: {
            expires: null //7, // Days or Date; null: session cookie
            //		path: "/", // Defaults to current page
            //		domain: "jquery.com",
            //		secure: true
        },
        // Class names used, when rendering the HTML markup.
        // Note: if only single entries are passed for options.classNames, all other
        // values are still set to default.
        classNames: {
            container: "dynatree-container",
            node: "dynatree-node",
            folder: "dynatree-folder",
            //		document: "dynatree-document",

            empty: "dynatree-empty",
            vline: "dynatree-vline",
            expander: "dynatree-expander",
            connector: "dynatree-connector",
            checkbox: "dynatree-checkbox",
            nodeIcon: "dynatree-icon",
            title: "dynatree-title",
            noConnector: "dynatree-no-connector",

            nodeError: "dynatree-statusnode-error",
            nodeWait: "dynatree-statusnode-wait",
            hidden: "dynatree-hidden",
            combinedExpanderPrefix: "dynatree-exp-",
            combinedIconPrefix: "dynatree-ico-",
            nodeLoading: "dynatree-loading",
            //		disabled: "dynatree-disabled",
            hasChildren: "dynatree-has-children",
            active: "dynatree-active",
            selected: "dynatree-selected",
            expanded: "dynatree-expanded",
            lazy: "dynatree-lazy",
            focused: "dynatree-focused",
            partsel: "dynatree-partsel",
            lastsib: "dynatree-lastsib"
        },
        debugLevel: 1,

        // ------------------------------------------------------------------------
        lastentry: undefined
    };
    //
    if (parseFloat($.ui.version) < 1.8) {
        $.ui.dynatree.defaults = $.ui.dynatree.prototype.options;
    }

    /*******************************************************************************
    * Reserved data attributes for a tree node.
    */
    $.ui.dynatree.nodedatadefaults = {
        title: null, // (required) Displayed name of the node (html is allowed here)
        key: null, // May be used with activate(), select(), find(), ...
        isFolder: false, // Use a folder icon. Also the node is expandable but not selectable.
        isLazy: false, // Call onLazyRead(), when the node is expanded for the first time to allow for delayed creation of children.
        tooltip: null, // Show this popup text.
        icon: null, // Use a custom image (filename relative to tree.options.imagePath). 'null' for default icon, 'false' for no icon.
        addClass: null, // Class name added to the node's span tag.
        noLink: false, // Use <span> instead of <a> tag for this node
        activate: false, // Initial active status.
        focus: false, // Initial focused status.
        expand: false, // Initial expanded status.
        select: false, // Initial selected status.
        hideCheckbox: false, // Suppress checkbox display for this node.
        unselectable: false, // Prevent selection.
        //  disabled: false,
        // The following attributes are only valid if passed to some functions:
        children: null, // Array of child nodes.
        // NOTE: we can also add custom attributes here.
        // This may then also be used in the onActivate(), onSelect() or onLazyTree() callbacks.
        // ------------------------------------------------------------------------
        lastentry: undefined
    };

    /*******************************************************************************
    * Drag and drop support
    */
    function _initDragAndDrop(tree) {
        var dnd = tree.options.dnd || null;
        // Register 'connectToDynatree' option with ui.draggable
        if (dnd && (dnd.onDragStart || dnd.onDrop)) {
            _registerDnd();
        }
        // Attach ui.draggable to this Dynatree instance
        if (dnd && dnd.onDragStart) {
            tree.$tree.draggable({
                addClasses: false,
                appendTo: "body",
                containment: false,
                delay: 0,
                distance: 4,
                revert: false,
                // Delegate draggable.start, drag, and stop events to our handler
                connectToDynatree: true,
                // Let source tree create the helper element
                helper: function (event) {
                    var sourceNode = getDtNodeFromElement(event.target);
                    return sourceNode.tree._onDragEvent("helper", sourceNode, null, event, null, null);
                },
                _last: null
            });
        }
        // Attach ui.droppable to this Dynatree instance
        if (dnd && dnd.onDrop) {
            tree.$tree.droppable({
                addClasses: false,
                tolerance: "intersect",
                greedy: false,
                _last: null
            });
        }
    }

    //--- Extend ui.draggable event handling --------------------------------------
    var didRegisterDnd = false;
    var _registerDnd = function () {
        if (didRegisterDnd) {
            return;
        }
        $.ui.plugin.add("draggable", "connectToDynatree", {
            start: function (event, ui) {
                var draggable = $(this).data("draggable");
                var sourceNode = ui.helper.data("dtSourceNode") || null;
                //			logMsg("draggable-connectToDynatree.start, %s", sourceNode);
                //			logMsg("    this: %o", this);
                //			logMsg("    event: %o", event);
                //			logMsg("    draggable: %o", draggable);
                //			logMsg("    ui: %o", ui);
                if (sourceNode) {
                    // Adjust helper offset, so cursor is slightly outside top/left corner
                    //				draggable.offset.click.top -= event.target.offsetTop;
                    //				draggable.offset.click.left -= event.target.offsetLeft;
                    draggable.offset.click.top = -2;
                    draggable.offset.click.left = +16;
                    //				logMsg("    draggable.offset.click FIXED: %s/%s", draggable.offset.click.left, draggable.offset.click.top);
                    // Trigger onDragStart event
                    // TODO: when called as connectTo..., the return value is ignored(?)
                    return sourceNode.tree._onDragEvent("start", sourceNode, null, event, ui, draggable);
                }
            },
            drag: function (event, ui) {
                var draggable = $(this).data("draggable");
                var sourceNode = ui.helper.data("dtSourceNode") || null;
                var prevTargetNode = ui.helper.data("dtTargetNode") || null;
                var targetNode = getDtNodeFromElement(event.target);
                //			logMsg("getDtNodeFromElement(%o): %s", event.target, targetNode);
                if (event.target && !targetNode) {
                    // We got a drag event, but the targetNode could not be found
                    // at the event location. This may happen, if the mouse
                    // jumped over the drag helper, in which case we ignore it:
                    var isHelper = $(event.target).closest("div.dynatree-drag-helper,#dynatree-drop-marker").length > 0;
                    if (isHelper) {
                        //					logMsg("Drag event over helper: ignored.");
                        return;
                    }
                }
                //			logMsg("draggable-connectToDynatree.drag: targetNode(from event): %s, dtTargetNode: %s", targetNode, ui.helper.data("dtTargetNode"));
                ui.helper.data("dtTargetNode", targetNode);
                // Leaving a tree node
                if (prevTargetNode && prevTargetNode !== targetNode) {
                    prevTargetNode.tree._onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable);
                }
                if (targetNode) {
                    if (!targetNode.tree.options.dnd.onDrop) {
                        // not enabled as drop target
                        noop(); // Keep JSLint happy
                    } else if (targetNode === prevTargetNode) {
                        // Moving over same node
                        targetNode.tree._onDragEvent("over", targetNode, sourceNode, event, ui, draggable);
                    } else {
                        // Entering this node first time
                        targetNode.tree._onDragEvent("enter", targetNode, sourceNode, event, ui, draggable);
                    }
                }
                // else go ahead with standard event handling
            },
            stop: function (event, ui) {
                var draggable = $(this).data("draggable");
                var sourceNode = ui.helper.data("dtSourceNode") || null;
                var targetNode = ui.helper.data("dtTargetNode") || null;
                //			logMsg("draggable-connectToDynatree.stop: targetNode(from event): %s, dtTargetNode: %s", targetNode, ui.helper.data("dtTargetNode"));
                //			logMsg("draggable-connectToDynatree.stop, %s", sourceNode);
                var mouseDownEvent = draggable._mouseDownEvent;
                var eventType = event.type;
                //			logMsg("    type: %o, downEvent: %o, upEvent: %o", eventType, mouseDownEvent, event);
                //			logMsg("    targetNode: %o", targetNode);
                var dropped = (eventType == "mouseup" && event.which == 1);
                if (!dropped) {
                    logMsg("Drag was cancelled");
                }
                if (targetNode) {
                    if (dropped) {
                        targetNode.tree._onDragEvent("drop", targetNode, sourceNode, event, ui, draggable);
                    }
                    targetNode.tree._onDragEvent("leave", targetNode, sourceNode, event, ui, draggable);
                }
                if (sourceNode) {
                    sourceNode.tree._onDragEvent("stop", sourceNode, null, event, ui, draggable);
                }
            }
        });
        didRegisterDnd = true;
    };

    // ---------------------------------------------------------------------------
})(jQuery);
